initial commit
This commit is contained in:
@@ -0,0 +1,264 @@
|
||||
"""
|
||||
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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user