""" 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 """ """ % (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('(?' + 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