Skip to content
This repository was archived by the owner on Jun 7, 2023. It is now read-only.

Commit 05089c9

Browse files
author
Brad Miller
committed
Add autograde to spreadsheet
1 parent 9510adf commit 05089c9

4 files changed

Lines changed: 142 additions & 10 deletions

File tree

.jshintrc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"esversion": 6
3+
4+
}
Lines changed: 119 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,132 @@
11
ssList = {};
22

3-
class SpreadSheet {
3+
class SpreadSheet extends RunestoneBase {
44
constructor(opts) {
5+
super(SpreadSheet);
56
let orig = opts.orig;
67
this.div_id = orig.id;
78
this.sheet_id = `${this.div_id}_sheet`;
89
this.data = eval(`${this.div_id}_data`);
10+
this.autograde = $(orig).data("autograde");
11+
this.suffix = window[`${this.div_id}_asserts`];
12+
this.renderSheet();
913

10-
this.renderSheet()
14+
if (this.autograde) {
15+
this.addAutoGradeButton();
16+
this.addOutput();
17+
}
1118
}
1219

1320
renderSheet() {
14-
let div = document.getElementById(this.sheet_id)
15-
this.table = jexcel(div, {data:this.data})
21+
let div = document.getElementById(this.sheet_id);
22+
this.table = jexcel(div, {data:this.data});
1623
}
24+
25+
addAutoGradeButton() {
26+
let div = document.getElementById(this.div_id);
27+
var butt = document.createElement("button");
28+
$(butt).text("Check");
29+
$(butt).addClass("btn btn-success run-button");
30+
div.appendChild(butt);
31+
this.gradeButton = butt;
32+
$(butt).click(this.doAutoGrade.bind(this));
33+
$(butt).attr("type","button");
34+
}
35+
36+
addOutput() {
37+
this.output = document.createElement('pre');
38+
this.output.id = this.divid+'_stdout';
39+
$(this.output).css("visibility","hidden");
40+
let div = document.getElementById(this.div_id);
41+
div.appendChild(this.output);
42+
}
43+
44+
doAutoGrade () {
45+
let tests = this.suffix;
46+
this.passed = 0;
47+
this.failed = 0;
48+
// Tests should be of the form
49+
// assert row,col oper value for example
50+
// assert 4,4 == 3
51+
let result = "";
52+
tests = tests.filter(function(s) {
53+
return s.indexOf('assert') > -1;
54+
});
55+
for (let test of tests) {
56+
let assert, loc, oper, expected;
57+
[assert, loc, oper, expected] = test.split(/\s+/);
58+
result += this.testOneAssert(loc, oper, expected);
59+
result += "\n";
60+
}
61+
let pct = 100 * this.passed / (this.passed + this.failed);
62+
pct = pct.toLocaleString(undefined, { maximumFractionDigits: 2});
63+
result += `You passed ${this.passed} out of ${this.passed+this.failed} tests for ${pct}%`;
64+
this.logBookEvent({event: 'unittest',
65+
div_id: this.divid,
66+
course: eBookConfig.course,
67+
act: `percent:${pct}:passed:${this.passed}:failed:${this.failed}`
68+
});
69+
$(this.output).css("visibility","visible");
70+
$(this.output).text(result);
71+
}
72+
73+
testOneAssert(cell, oper, expected) {
74+
let actual = this.getCellDisplayValue(cell);
75+
const operators = {
76+
"==" : function (operand1, operand2) {
77+
return operand1 == operand2;
78+
},
79+
"!=" : function (operand1, operand2) {
80+
return operand1 != operand2;
81+
},
82+
">" : function (operand1, operand2) {
83+
return operand1 > operand2;
84+
},
85+
"<" : function (operand1, operand2) {
86+
return operand1 > operand2;
87+
}
88+
};
89+
90+
let res = operators[oper](actual, expected);
91+
let output = "";
92+
if (res) {
93+
output = `Pass: ${actual} ${oper} ${expected} in ${cell}`;
94+
this.passed++;
95+
} else {
96+
output = `Failed ${actual} ${oper} ${expected} in cell ${cell}`;
97+
this.failed++;
98+
}
99+
return output;
100+
}
101+
102+
103+
104+
// If the cell contains a formula, this call will return the formula not the computed value
105+
getCellSource(cell) {
106+
return this.table.getValue(cell);
107+
}
108+
109+
// If the cell contains a formula this call will return the computed value
110+
getCellDisplayValue(cell) {
111+
let parts = cell.match(/\$?([A-Z]+)\$?([0-9]+)/);
112+
let x = this.columnToIndex(parts[1]);
113+
let y = parts[2] - 1;
114+
let res = this.table.el.querySelector(`[data-x="${x}"][data-y="${y}"]`);
115+
return res.innerText;
116+
}
117+
118+
columnToIndex(colName) {
119+
// Convert the column name to a number A = 0 AA = 26 BA = 52, etc
120+
let base = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
121+
let result = 0;
122+
123+
for (let i = 0, j = colName.length - 1; i < colName.length; i += 1, j -= 1) {
124+
result += Math.pow(base.length, j) * (base.indexOf(colName[i]) + 1);
125+
}
126+
127+
return result - 1;
128+
}
129+
17130
}
18131

19132
$(document).bind("runestone:login-complete", function () {
@@ -24,6 +137,6 @@ $(document).bind("runestone:login-complete", function () {
24137
});
25138

26139
if (typeof component_factory === 'undefined') {
27-
component_factory = {}
140+
component_factory = {};
28141
}
29-
component_factory['spreadsheet'] = function(opts) { return new SpreadSheet(opts)}
142+
component_factory.spreadsheet = function(opts) { return new SpreadSheet(opts);};

runestone/spreadsheet/spreadsheet.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
#
1616
__author__ = 'bmiller'
1717

18-
18+
import re
1919
from docutils import nodes
2020
from docutils.parsers.rst import directives
2121
from runestone.common.runestonedirective import RunestoneNode, RunestoneIdDirective, get_node_line
@@ -67,6 +67,7 @@ class SpreadSheet(RunestoneIdDirective):
6767
:colwidths: list of column widths
6868
:coltitles: list of column names
6969
:coltypes: list of column types
70+
:mindimensions: mincols, minrows -- minDimensions:[10,5]
7071
7172
A1,B1,C1,D1...
7273
A2,B2,C2,D2...
@@ -89,6 +90,14 @@ def run(self):
8990

9091
self.options['divid'] = self.arguments[0].strip()
9192

93+
if '====' in self.content:
94+
idx = self.content.index('====')
95+
suffix = self.content[idx+1:]
96+
self.options['asserts'] = suffix
97+
self.options['autograde'] = 'data-autograde="true"'
98+
else:
99+
self.options['autograde'] = ''
100+
92101
if 'fromcsv' in self.options:
93102
self.content = self.body_from_csv(self.options['fromcsv'])
94103
else:
@@ -110,6 +119,8 @@ def run(self):
110119
def body_to_csv(self):
111120
csvlist = []
112121
for row in self.content:
122+
if re.match(r'^\s*====', row):
123+
break
113124
items = row.split(',')
114125
ilist = []
115126
for item in items:
@@ -143,12 +154,12 @@ def as_int_or_float(s):
143154

144155

145156
TEMPLATE = """
146-
<div id="{divid}" data-component="spreadsheet">
157+
<div id="{divid}" data-component="spreadsheet" class="runestone" {autograde}>
147158
<div id="{divid}_sheet"></div>
148159
149160
<script>
150-
{divid}_data = {data}
151-
161+
{divid}_data = {data};
162+
{divid}_asserts = {asserts};
152163
</script>
153164
</div>
154165
"""

runestone/spreadsheet/test/_sources/index.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,7 @@ SECTION 1: Spreadsheets
2323
Yahoo, 1994, 38.66
2424
,,=sum(c1:c3)
2525

26+
====
27+
assert A3 == Yahoo
28+
assert B3 == 1994
29+

0 commit comments

Comments
 (0)