267 lines
9.1 KiB
Python
267 lines
9.1 KiB
Python
"""
|
|
Developed by Massimo Di Pierro, optional component of web2py, GPL2 license.
|
|
"""
|
|
import re
|
|
import pickle
|
|
import copy
|
|
|
|
|
|
def quote(text):
|
|
return str(text).replace('\\', '\\\\').replace("'", "\\'")
|
|
|
|
|
|
class Node:
|
|
"""
|
|
Example::
|
|
|
|
# controller
|
|
from gluon.contrib.spreadsheet import Sheet
|
|
|
|
def callback():
|
|
return cache.ram('sheet1', lambda: None, None).process(request)
|
|
|
|
def index():
|
|
sheet = cache.ram('sheet1',
|
|
lambda: Sheet(10, 10, URL(r=request, f='callback')), 0)
|
|
#sheet.cell('r0c3', value='=r0c0+r0c1+r0c2', readonly=True)
|
|
return dict(sheet=sheet)
|
|
|
|
# view
|
|
{{extend 'layout.html'}}
|
|
{{=sheet}}
|
|
|
|
or insert invidivual cells via
|
|
|
|
{{=sheet.nodes['r0c0']}}
|
|
|
|
"""
|
|
|
|
def __init__(self, name, value, url='.', readonly=False, active=True,
|
|
onchange=None):
|
|
self.url = url
|
|
self.name = name
|
|
self.value = str(value)
|
|
self.computed_value = ''
|
|
self.incoming = {}
|
|
self.outcoming = {}
|
|
self.readonly = readonly
|
|
self.active = active
|
|
self.onchange = onchange
|
|
self.size = 4
|
|
self.locked = False
|
|
|
|
def xml(self):
|
|
return """<input name="%s" id="%s" value="%s" size="%s"
|
|
onkeyup="ajax('%s/keyup',['%s'], ':eval');"
|
|
onfocus="ajax('%s/focus',['%s'], ':eval');"
|
|
onblur="ajax('%s/blur',['%s'], ':eval');" %s/>
|
|
""" % (self.name, self.name, self.computed_value, self.size,
|
|
self.url, self.name, self.url, self.name, self.url, self.name,
|
|
(self.readonly and 'readonly ') or '')
|
|
|
|
def __repr__(self):
|
|
return '%s:%s' % (self.name, self.computed_value)
|
|
|
|
|
|
class Sheet:
|
|
|
|
regex=re.compile('(?<!\w)[a-zA-Z_]\w*')
|
|
|
|
re_strings = re.compile(r'(?P<name>'
|
|
+ r"[uU]?[rR]?'''([^']+|'{1,2}(?!'))*'''|"
|
|
+ r"'([^'\\]|\\.)*'|"
|
|
+ r'"""([^"]|"{1,2}(?!"))*"""|'
|
|
+ r'"([^"\\]|\\.)*")', re.DOTALL)
|
|
|
|
def dumps(self):
|
|
dump = pickle.dumps(self)
|
|
return dump
|
|
|
|
@staticmethod
|
|
def loads(data):
|
|
sheet = pickle.loads(data)
|
|
return sheet
|
|
|
|
def process(self, request):
|
|
"""
|
|
call this in action that creates table, it will handle ajax callbacks
|
|
"""
|
|
cell = request.vars.keys()[0]
|
|
if request.args(0) == 'focus':
|
|
return "jQuery('#%s').val('%s');" % (cell, quote(self[cell].value))
|
|
value = request.vars[cell]
|
|
self[cell] = value
|
|
if request.args(0) == 'blur':
|
|
return "jQuery('#%s').val('%s');" \
|
|
% (cell, quote(self[cell].computed_value))
|
|
elif request.args(0) == 'keyup':
|
|
jquery = ''
|
|
for other_key in self.modified:
|
|
if other_key != cell:
|
|
jquery += "jQuery('#%s').val('%s');" % \
|
|
(other_key, quote(self[other_key].computed_value))
|
|
return jquery
|
|
|
|
def __init__(self, rows, cols, url='.', readonly=False, active=True,
|
|
onchange=None):
|
|
self.rows = rows
|
|
self.cols = cols
|
|
self.url = url
|
|
self.nodes = {}
|
|
self.error = 'ERROR: %(error)s'
|
|
self.allowed_keywords = ['for', 'in', 'if', 'else', 'and', 'or', 'not',
|
|
'i', 'j', 'k', 'x', 'y', 'z', 'sum']
|
|
self.environment = {}
|
|
[self.cell('r%sc%s'%(k/cols, k%cols), '0.0', readonly, active, onchange)
|
|
for k in xrange(rows*cols)]
|
|
exec('from math import *', {}, self.environment)
|
|
|
|
def delete_from(self, other_list):
|
|
indices = [k for (k, node) in enumerate(other_list) if k == node]
|
|
if indices:
|
|
del other_list[indices[0]]
|
|
|
|
def changed(self, node, changed_nodes=[]):
|
|
for other_node in node.outcoming:
|
|
if not other_node in changed_nodes:
|
|
changed_nodes.append(other_node)
|
|
self.changed(other_node, changed_nodes)
|
|
return changed_nodes
|
|
|
|
def define(self, name, obj):
|
|
self.environment[name] = obj
|
|
|
|
def cell(self, key, value, readonly=False, active=True, onchange=None):
|
|
"""
|
|
key is the name of the cell
|
|
value is the initial value of the cell. It can be a formula "=1+3"
|
|
a cell is active if it evaluates formuls
|
|
"""
|
|
key = str(key)
|
|
if not self.regex.match(key):
|
|
raise SyntaxError, "Invalid cell name: %s" % key
|
|
node = Node(key, value, self.url, readonly, active, onchange)
|
|
self.nodes[key] = node
|
|
self[key] = value
|
|
|
|
def __setitem__(self, key, value):
|
|
key = str(key)
|
|
value = str(value)
|
|
node = self.nodes[key]
|
|
node.value = value
|
|
if value[:1] == '=' and node.active:
|
|
# clear all edges involving current node
|
|
for other_node in node.incoming:
|
|
del other_node.outcoming[node]
|
|
node.incoming.clear()
|
|
# build new edges
|
|
command = self.re_strings.sub("''", value[1:])
|
|
node.locked = False
|
|
for match in self.regex.finditer(command):
|
|
other_key = match.group()
|
|
if other_key == key:
|
|
self.computed_value = self.error % dict(error='cycle')
|
|
self.modified={}
|
|
break
|
|
if other_key in self.nodes:
|
|
other_node = self.nodes[other_key]
|
|
other_node.outcoming[node] = True
|
|
node.incoming[other_node] = True
|
|
elif not other_key in self.allowed_keywords and \
|
|
not other_key in self.environment:
|
|
node.locked = True
|
|
node.computed_value = \
|
|
self.error % dict(error='invalid keyword: ' + other_key)
|
|
self.modified = {}
|
|
break
|
|
self.compute(node)
|
|
else:
|
|
try:
|
|
node.computed_value = int(node.value)
|
|
except:
|
|
try:
|
|
node.computed_value = float(node.value)
|
|
except:
|
|
node.computed_value = node.value
|
|
self.environment[key] = node.computed_value
|
|
if node.onchange:
|
|
node.onchange(node)
|
|
self.modified = self.iterate(node)
|
|
|
|
def compute(self, node):
|
|
if node.value[:1] == '=' and not node.locked:
|
|
try:
|
|
exec('__value__=' + node.value[1:], {}, self.environment)
|
|
node.computed_value = self.environment['__value__']
|
|
del self.environment['__value__']
|
|
except Exception, e:
|
|
node.computed_value = self.error % dict(error=str(e))
|
|
self.environment[node.name] = node.computed_value
|
|
if node.onchange:
|
|
node.onchange(node)
|
|
|
|
def iterate(self, node):
|
|
output = {node.name: node.computed_value}
|
|
changed_nodes = self.changed(node)
|
|
while changed_nodes:
|
|
ok=False
|
|
set_changed_nodes = set(changed_nodes)
|
|
for (k, other_node) in enumerate(changed_nodes):
|
|
#print other_node, changed_nodes
|
|
if not set(other_node.incoming.keys()).\
|
|
intersection(set_changed_nodes):
|
|
#print 'ok'
|
|
self.compute(other_node)
|
|
output[other_node.name] = other_node.computed_value
|
|
#print other_node
|
|
del changed_nodes[k]
|
|
ok = True
|
|
break
|
|
if not ok:
|
|
return {}
|
|
return output
|
|
|
|
def __getitem__(self, key):
|
|
return self.nodes[str(key)]
|
|
|
|
def get_computed_values(self):
|
|
d={}
|
|
for key in self.nodes:
|
|
node = self.nodes[key]
|
|
if node.value[:1] != '=' or not node.active:
|
|
d[key] = node.computed_value
|
|
return d
|
|
|
|
def set_computed_values(self, d):
|
|
for key in d:
|
|
if not key in self.nodes:
|
|
continue
|
|
node = self.nodes[key]
|
|
if node.value[:1] != '=' or not node.active:
|
|
node.value = d[key]
|
|
|
|
def xml(self):
|
|
import gluon.html
|
|
(DIV, TABLE, TR, TD, TH, BR) = \
|
|
(gluon.html.DIV, gluon.html.TABLE, gluon.html.TR, gluon.html.TD,
|
|
gluon.html.TH, gluon.html.BR)
|
|
regex = re.compile('r\d+c\d+')
|
|
return DIV(TABLE(TR(TH(), *[TH('c%s' % c) for c in range(self.cols)]),
|
|
*[TR(TH('r%s' % r), *[TD(self.nodes['r%sc%s'%(r, c)]) \
|
|
for c in range(self.cols)]) \
|
|
for r in range(self.rows)]),
|
|
BR(),
|
|
TABLE(*[TR(TH(key), TD(self.nodes[key])) \
|
|
for key in self.nodes if not regex.match(key)])).xml()
|
|
|
|
if __name__ == '__main__':
|
|
s = Sheet(0, 0)
|
|
s.cell('a', value="2")
|
|
s.cell('b', value="=sin(a)")
|
|
s.cell('c', value="=cos(a)**2+b*b")
|
|
print s['c'].computed_value
|
|
|
|
|
|
|
|
|