improved spreadsheet, issue 860, thanks Alan
This commit is contained in:
@@ -1 +1 @@
|
||||
Version 2.00.0 (2012-06-25 14:31:49) dev
|
||||
Version 2.00.0 (2012-06-25 14:35:08) dev
|
||||
|
||||
+677
-64
@@ -1,43 +1,21 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Developed by Massimo Di Pierro, optional component of web2py, GPL2 license.
|
||||
"""
|
||||
|
||||
import re
|
||||
import pickle
|
||||
import copy
|
||||
|
||||
import simplejson
|
||||
|
||||
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):
|
||||
onchange=None, **kwarg):
|
||||
self.url = url
|
||||
self.name = name
|
||||
self.value = str(value)
|
||||
@@ -49,7 +27,7 @@ class Node:
|
||||
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');"
|
||||
@@ -64,9 +42,192 @@ class Node:
|
||||
|
||||
|
||||
class Sheet:
|
||||
"""
|
||||
Basic class for creating web spreadsheets
|
||||
|
||||
New features:
|
||||
|
||||
regex=re.compile('(?<!\w)[a-zA-Z_]\w*')
|
||||
-dal spreadsheets:
|
||||
It receives a Rows object instance and presents
|
||||
the selected data in a cell per field basis (table rows
|
||||
are sheet rows as well)
|
||||
Input should be short extension data as numeric data
|
||||
or math expressions but can be anything supported by
|
||||
unicode.
|
||||
|
||||
-row(), column() and matrix() class methods:
|
||||
These new methods allow to set bulk data sets
|
||||
without calling .cell() for each node
|
||||
Example::
|
||||
|
||||
# controller
|
||||
from gluon.contrib.spreadsheet import Sheet
|
||||
|
||||
def callback():
|
||||
return cache.ram('sheet1', lambda: None, None).process(request)
|
||||
|
||||
def index():
|
||||
# standard spreadsheet method
|
||||
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)
|
||||
|
||||
def index():
|
||||
# database spreadsheet method
|
||||
sheet = cache.ram('sheet1',
|
||||
lambda: Sheet(10, 10, URL(r=request, f='callback'),
|
||||
data=db(db.mydata).select()), 0)
|
||||
return dict(sheet=sheet)
|
||||
|
||||
# view
|
||||
{{extend 'layout.html'}}
|
||||
{{=sheet}}
|
||||
|
||||
or insert invidivual cells via
|
||||
|
||||
{{=sheet.nodes['r0c0']}}
|
||||
|
||||
Sheet stores a JavaScript w2p_spreadsheet_data object
|
||||
for retrieving data updates from the client.
|
||||
|
||||
The data structure of the js object is as follows:
|
||||
# columns: a dict with colname, column index map
|
||||
# colnames: a dict with column index, colname map
|
||||
# id_columns: list with id columns
|
||||
# id_colnames: dict with id colname: column index map
|
||||
# cells: dict of "rncn": value pairs
|
||||
# modified: dict of modified cells for client-side
|
||||
|
||||
Also, there is a class method Sheet.update(data) that
|
||||
processes the json data as sent by the client and
|
||||
returns a set of db modifications (see the method help for
|
||||
more details)
|
||||
|
||||
client JavaScript objects:
|
||||
|
||||
-var w2p_spreadsheet_data
|
||||
Stores cell updates by key and
|
||||
Used for updated cells control
|
||||
|
||||
-var w2p_spreadsheet_update_button
|
||||
Stores the id of the update command
|
||||
Used for event binding (update click)
|
||||
|
||||
var w2p_spreadsheet_update_result
|
||||
object attributes:
|
||||
modified - n updated records
|
||||
errors - n errors
|
||||
message - a message for feedback and errors
|
||||
|
||||
Stores the ajax db update call returned stats
|
||||
and the db_callback string js
|
||||
Used after calling w2p_spreadsheet_update_db()
|
||||
|
||||
-function w2p_spreadsheet_update_cell(a)
|
||||
Used for responding to normal cell events
|
||||
(encapsulates old behavior)
|
||||
|
||||
-function w2p_spreadsheet_update_db_callback(result)
|
||||
Called after a background db update
|
||||
|
||||
-function w2p_spreadsheet_update_db()
|
||||
Called for updating the database with
|
||||
client data
|
||||
|
||||
First method: Sending data trough a form helper:
|
||||
(the data payload must be inserted in a form field before
|
||||
submission)
|
||||
|
||||
-Applying db changes made client-side
|
||||
|
||||
Example controller:
|
||||
...
|
||||
# present a submit button with the spreadsheet
|
||||
form = SQLFORM.factory(Field("<name>",
|
||||
"text",
|
||||
readable=False, writable=False,
|
||||
formname="<formname>"))
|
||||
# submit button label
|
||||
form.elements("input [type=submit]").attributes["_value"] = \
|
||||
T("Update database")
|
||||
form.elements("textarea")[0].attributes["_style"] = "display: none;"
|
||||
|
||||
w2p_spreadsheet_update_script = SCRIPT('''
|
||||
jQuery(
|
||||
function(){
|
||||
jQuery("<formname>").submit(function(){
|
||||
jQuery("[name=<name>]").val(JSON.stringify(
|
||||
w2p_spreadsheet_data)
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
||||
''')
|
||||
|
||||
# retrieve changes
|
||||
if form.process().accepted:
|
||||
data = form.vars.<name>
|
||||
changes = Sheet.updated(data)
|
||||
|
||||
# Do db I/O:
|
||||
for table, rows in changes.iteritems():
|
||||
for row, values in rows.iteritems():
|
||||
db[table][row].update_record(**values)
|
||||
|
||||
# the action view should expose {{=form}}, {{=sheet}}, {{=myscript}}
|
||||
return dict(form=form, sheet=sheet,
|
||||
myscript=w2p_spreadseet_update_script)
|
||||
|
||||
Second method: Sending data updates with .ajax()
|
||||
|
||||
-spreadsheet page's view:
|
||||
|
||||
{{
|
||||
=INPUT(_type="button", _value="update data",
|
||||
_id="w2p_spreadsheet_update_data")
|
||||
}}
|
||||
|
||||
{{=SCRIPT('''
|
||||
jQuery(function(){
|
||||
jQuery("#w2p_spreadsheet_update_data").click(
|
||||
function(){
|
||||
jQuery.ajax({url: "%s",
|
||||
type: "POST",
|
||||
data:
|
||||
{data:
|
||||
JSON.stringify(w2p_spreadsheet_data)}
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
''' % URL(c="default", f="modified"))}}
|
||||
|
||||
-controller: modified function
|
||||
def modified():
|
||||
data = request.vars.data
|
||||
changes = Sheet.updated(data)
|
||||
# (for db I/O see first method)
|
||||
return "ok"
|
||||
|
||||
|
||||
Third method:
|
||||
When creating a Sheet instance, pass a kwarg update_button=<button id>
|
||||
This step will auto process the updated data with db I/O (requires calling
|
||||
.process() with db=<DAL instance>). You must expose an html element which
|
||||
supports the .click() event, i.e. a normal button.
|
||||
|
||||
# TODO:
|
||||
# -¿SIGNED URLS?
|
||||
# -Delete checkbox columns for each table and default
|
||||
# -Deletable=True option for showing/hiding delete checkboxes
|
||||
# -process() method support for db I/O
|
||||
|
||||
"""
|
||||
|
||||
regex = re.compile('(?<!\w)[a-zA-Z_]\w*')
|
||||
pregex = re.compile('\d+')
|
||||
re_strings = re.compile(r'(?P<name>'
|
||||
+ r"[uU]?[rR]?'''([^']+|'{1,2}(?!'))*'''|"
|
||||
+ r"'([^'\\]|\\.)*'|"
|
||||
@@ -77,33 +238,180 @@ class Sheet:
|
||||
dump = pickle.dumps(self)
|
||||
return dump
|
||||
|
||||
@staticmethod
|
||||
def position(key):
|
||||
""" Returns int row, int col for a 'rncn' formatted key'"""
|
||||
try:
|
||||
r, c = Sheet.pregex.findall(key)
|
||||
r, c = int(r), int(c)
|
||||
except (ValueError, IndexError, TypeError), e:
|
||||
error = "%s. %s" % \
|
||||
("Unexpected position parameter",
|
||||
"Must be a key of type 'rncn'")
|
||||
raise ValueError(error)
|
||||
return r, c
|
||||
|
||||
@staticmethod
|
||||
def loads(data):
|
||||
sheet = pickle.loads(data)
|
||||
return sheet
|
||||
|
||||
def process(self, request):
|
||||
@staticmethod
|
||||
def updated(data):
|
||||
""" Reads spreadsheet update information sent client-side.
|
||||
|
||||
Returns a dict with updated database rows/fields.
|
||||
Structure:
|
||||
{<tablename>:{
|
||||
<id>:{<fieldname>:<new value>,
|
||||
<fieldname>:<new value>,
|
||||
...
|
||||
},
|
||||
...
|
||||
}
|
||||
}
|
||||
|
||||
data dict argument:
|
||||
|
||||
# columns: (a dict with colname, column index map)
|
||||
# colnames: (a dict with column index, colname map)
|
||||
# id_columns: list with id columns
|
||||
# id_colnames: dict with id colname: column index map
|
||||
# cells: dict of "rncn": value pairs
|
||||
# modified: dict of modified cells for client-side
|
||||
|
||||
"""
|
||||
data = simplejson.loads(data)
|
||||
|
||||
# record update dict
|
||||
changes = {}
|
||||
|
||||
# read column index per table
|
||||
# table, id column map
|
||||
tablenames = {}
|
||||
for colname, i in data["id_colnames"].iteritems():
|
||||
tablenames[colname.split(".")[0]] = i
|
||||
|
||||
# Separate multitable rows
|
||||
# Identify each row id (old data)
|
||||
# Build a dict with table/row/field
|
||||
# update information.
|
||||
|
||||
# collect new data by row (modified):
|
||||
for key, value in data["modified"].iteritems():
|
||||
r, c = Sheet.position(key)
|
||||
|
||||
# don't apply repeated values
|
||||
if data["cells"][key] != value:
|
||||
# read tablename
|
||||
tablename, fieldname = data["colnames"][str(c)].split(".")
|
||||
|
||||
# read db record id
|
||||
row_id_key = "r%sc%s" % (r, tablenames[tablename])
|
||||
row_id = data["cells"][row_id_key]
|
||||
|
||||
changes.setdefault(tablename, {})
|
||||
changes[tablename].setdefault(row_id, {})
|
||||
changes[tablename][row_id][fieldname] = value
|
||||
|
||||
return changes
|
||||
|
||||
|
||||
def process(self, request, db=None, db_callback=None):
|
||||
"""
|
||||
call this in action that creates table, it will handle ajax callbacks
|
||||
optional db (a DAL instance). It's required for db I/O
|
||||
optional callback string. js commands to call after successful
|
||||
ajax db update.
|
||||
db_callback string format keys:
|
||||
modified (number of rows updated)
|
||||
"""
|
||||
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))
|
||||
|
||||
if not request.args(0) == "data":
|
||||
# normal cell processing
|
||||
cell = request.vars.keys()[0]
|
||||
|
||||
if request.args(0) == 'focus':
|
||||
return "jQuery('#%(cell)s').val('%(value)s');" % \
|
||||
dict(cell=cell, value=quote(self[cell].value))
|
||||
|
||||
value = request.vars[cell]
|
||||
self[cell] = value
|
||||
|
||||
if request.args(0) == 'blur':
|
||||
return "jQuery('#%(cell)s').val('%(value)s');" % \
|
||||
dict(cell=cell, value=quote(self[cell].computed_value))
|
||||
|
||||
elif request.args(0) == 'keyup':
|
||||
jquery = ''
|
||||
for other_key in self.modified:
|
||||
if other_key != cell:
|
||||
jquery += "jQuery('#%(other_key)s').val('%(value)s');" % \
|
||||
dict(other_key=other_key,
|
||||
value=quote(self[other_key].computed_value))
|
||||
|
||||
else:
|
||||
# spreadsheet db update
|
||||
result = dict(modified=0,
|
||||
errors=0,
|
||||
message="",
|
||||
db_callback="")
|
||||
|
||||
if db is not None:
|
||||
data = request.vars["data"]
|
||||
changes = self.updated(data)
|
||||
|
||||
# Do db I/O:
|
||||
for table, rows in changes.iteritems():
|
||||
for row, values in rows.iteritems():
|
||||
db[table][row].update_record(**values)
|
||||
result["modified"] += 1
|
||||
|
||||
if db_callback is not None:
|
||||
result["db_callback"] = db_callback
|
||||
else:
|
||||
result["message"] = "Sheet.process Error. No db found."
|
||||
return simplejson.dumps(result)
|
||||
|
||||
return jquery
|
||||
|
||||
def __init__(self, rows, cols, url='.', readonly=False, active=True,
|
||||
onchange=None):
|
||||
def get_attributes(self, data):
|
||||
attributes = {}
|
||||
for k in data.keys():
|
||||
if k.startswith("_"):
|
||||
attributes[k] = data[k]
|
||||
return attributes
|
||||
|
||||
def even_or_odd(self, v):
|
||||
""" Used for table row stripping """
|
||||
if v % 2 == 0:
|
||||
return "even"
|
||||
else:
|
||||
return "odd"
|
||||
|
||||
def __init__(self, rows, cols, url='.', readonly=False,
|
||||
active=True, onchange=None, value=None, data=None,
|
||||
headers=None, update_button="", **kwarg):
|
||||
|
||||
"""
|
||||
Arguments:
|
||||
headers: a dict with "table.fieldname": name values
|
||||
value: common value for all spreadsheet
|
||||
(can be a lambda x, y: z or function reference)
|
||||
|
||||
Rows and cols values will be updated automatically to fit
|
||||
the data boundaries when the data argument is a Rows object.
|
||||
|
||||
self.client: for storing sheet data client side
|
||||
columns: a dict with colname, column index map
|
||||
colnames: a dict with column index, colname map
|
||||
id_columns: list with id columns
|
||||
id_colnames: dict with id colname: column index map
|
||||
cells: dict of "rncn": value pairs
|
||||
modified: dict of modified cells for client-side
|
||||
edition.
|
||||
"""
|
||||
|
||||
self.rows = rows
|
||||
self.cols = cols
|
||||
self.url = url
|
||||
@@ -111,9 +419,74 @@ class Sheet:
|
||||
self.error = 'ERROR: %(error)s'
|
||||
self.allowed_keywords = ['for', 'in', 'if', 'else', 'and', 'or', 'not',
|
||||
'i', 'j', 'k', 'x', 'y', 'z', 'sum']
|
||||
self.value = value
|
||||
self.environment = {}
|
||||
[self.cell('r%sc%s'%(k/cols, k%cols), '0.0', readonly, active, onchange)
|
||||
for k in xrange(rows*cols)]
|
||||
self.attributes = self.get_attributes(kwarg)
|
||||
self.tr_attributes = {}
|
||||
self.td_attributes = {}
|
||||
|
||||
self.data = data
|
||||
self.readonly = readonly
|
||||
|
||||
self.update_button = update_button
|
||||
|
||||
self.client = {
|
||||
"columns": {},
|
||||
"colnames": {},
|
||||
"id_columns": [],
|
||||
"id_colnames": {},
|
||||
"cells": {},
|
||||
"modified": {},
|
||||
"headers": headers
|
||||
}
|
||||
|
||||
# if db and query:
|
||||
if self.data is not None:
|
||||
# retrieve row columns length
|
||||
self.rows = len(self.data)
|
||||
# retrieve rows length
|
||||
self.cols = len(self.data.colnames)
|
||||
|
||||
# map row data to rncn values
|
||||
for x, colname in enumerate(self.data.colnames):
|
||||
self.client["columns"][colname] = x
|
||||
self.client["colnames"][x] = colname
|
||||
|
||||
for x, row in enumerate(self.data):
|
||||
for colname, y in self.client["columns"].iteritems():
|
||||
key = "r%sc%s" % (x, y)
|
||||
tablename, fieldname = colname.split(".")
|
||||
try:
|
||||
value = row[tablename][fieldname]
|
||||
except (KeyError, AttributeError):
|
||||
# single table query
|
||||
value = row[fieldname]
|
||||
self.client["cells"][key] = str(value)
|
||||
# TODO: support different id keys
|
||||
if ".id" in colname:
|
||||
self.client["id_columns"].append(y)
|
||||
self.client["id_colnames"][colname] = y
|
||||
|
||||
for k in xrange(self.rows*self.cols):
|
||||
key = 'r%sc%s'%(k/self.cols, k%self.cols)
|
||||
r, c = self.position(key)
|
||||
if self.client["cells"].has_key(key):
|
||||
value = self.client["cells"][key]
|
||||
# readonly id values
|
||||
if c in self.client["id_columns"]:
|
||||
readonly = True
|
||||
else:
|
||||
readonly = self.readonly
|
||||
elif self.value is not None:
|
||||
if callable(self.value):
|
||||
value = self.value(r, c)
|
||||
else:
|
||||
value = self.value
|
||||
else:
|
||||
value = '0.00'
|
||||
self.cell(key, value,
|
||||
readonly, active, onchange)
|
||||
|
||||
exec('from math import *', {}, self.environment)
|
||||
|
||||
def delete_from(self, other_list):
|
||||
@@ -131,19 +504,179 @@ class Sheet:
|
||||
def define(self, name, obj):
|
||||
self.environment[name] = obj
|
||||
|
||||
def cell(self, key, value, readonly=False, active=True, onchange=None):
|
||||
def cell(self, key, value, readonly=False, active=True,
|
||||
onchange=None, **kwarg):
|
||||
"""
|
||||
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
|
||||
a cell is active if it evaluates formulas
|
||||
|
||||
Value can be a function(r, c) which returns a string
|
||||
"""
|
||||
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)
|
||||
else:
|
||||
attributes = self.get_attributes(kwarg)
|
||||
if attributes is not None:
|
||||
self.td_attributes[key] = attributes
|
||||
|
||||
key = str(key)
|
||||
r, c = self.position(key)
|
||||
|
||||
if callable(value):
|
||||
value = value(r, c)
|
||||
|
||||
node = Node(key, value, self.url, readonly, active,
|
||||
onchange, **kwarg)
|
||||
self.nodes[key] = node
|
||||
self[key] = value
|
||||
|
||||
def get_cell_arguments(self, data, default=None):
|
||||
"""Reads cell arguments from a dict object"""
|
||||
active = True
|
||||
onchange = None
|
||||
readonly = False
|
||||
value = ""
|
||||
if default is not None:
|
||||
data.update(default)
|
||||
if "active" in data:
|
||||
active = data["active"]
|
||||
if "readonly" in data:
|
||||
readonly = data["readonly"]
|
||||
if "onchange" in data:
|
||||
onchange = data["onchange"]
|
||||
if "value" in data:
|
||||
value = data["value"]
|
||||
return active, onchange, readonly, value
|
||||
|
||||
def row(self, row, cells, value=None, **kwarg):
|
||||
# row: row index (0, 1, ...)
|
||||
# cells: a sequence of values or a dict of dict with
|
||||
# arg: value pairs
|
||||
# one column example:
|
||||
# {"0": {"value":1.0, "readonly":False, "active":True, "onchange":None}}
|
||||
# value: common value for all cells
|
||||
|
||||
attributes = self.get_attributes(kwarg)
|
||||
if attributes is not None:
|
||||
self.tr_attributes[str(row)] = attributes
|
||||
if isinstance(cells, dict):
|
||||
for col, data in cells.iteritems():
|
||||
key = "r%sc%s" % (row, col)
|
||||
active, onchange, readonly, cell_value = \
|
||||
self.get_cell_arguments(data, default=kwarg)
|
||||
if value is None:
|
||||
v = cell_value
|
||||
else:
|
||||
v = value
|
||||
self.cell(key, v, active=active,
|
||||
readonly=readonly,
|
||||
onchange=onchange, **attributes)
|
||||
else:
|
||||
active, onchange, readonly, all_value = \
|
||||
self.get_cell_arguments(kwarg)
|
||||
for col, cell_value in enumerate(cells):
|
||||
key = "r%sc%s" % (row, col)
|
||||
if value is None:
|
||||
v = cell_value
|
||||
else:
|
||||
v = value
|
||||
self.cell(key, v, active=active,
|
||||
onchange=onchange,
|
||||
readonly=readonly, **attributes)
|
||||
|
||||
def column(self, col, cells, value=None, **kwarg):
|
||||
"""
|
||||
# col: column index (0, 1, ...)
|
||||
# cells: a sequence of values or a dict of dict with
|
||||
# arg: value pairs
|
||||
# one row example:
|
||||
# {"0": {"value":1.0, "readonly":False, "active":True, "onchange":None}}
|
||||
# value: common value for all cells
|
||||
"""
|
||||
attributes = self.get_attributes(kwarg)
|
||||
|
||||
if isinstance(cells, dict):
|
||||
for row, data in cells.iteritems():
|
||||
key = "r%sc%s" % (row, col)
|
||||
active, onchange, readonly, cell_value = \
|
||||
self.get_cell_arguments(data, default=kwarg)
|
||||
if value is None:
|
||||
v = cell_value
|
||||
else:
|
||||
v = value
|
||||
self.cell(key, v, active=active, readonly=readonly,
|
||||
onchange=onchange, **attributes)
|
||||
else:
|
||||
active, onchange, readonly, all_value = \
|
||||
self.get_cell_arguments(kwarg)
|
||||
for row, cell_value in enumerate(cells):
|
||||
key = "r%sc%s" % (row, col)
|
||||
if value is None:
|
||||
v = cell_value
|
||||
else:
|
||||
v = value
|
||||
self.cell(key, v, active=active,
|
||||
onchange=onchange, readonly=readonly,
|
||||
**attributes)
|
||||
|
||||
def matrix(self, cells, starts="r0c0", ends=None, value=None, **kwarg):
|
||||
"""
|
||||
Insert a n x n matrix or a set of cells
|
||||
# starts: upper left cell
|
||||
# ends: lower right cell
|
||||
|
||||
# cells: a sequence of value sequences
|
||||
# or a dict with "rncn" keys
|
||||
# Example 1 cells:
|
||||
# ((v11, v12, ... v1n),
|
||||
(vn2, vn2, ... vnn))
|
||||
# Example 2 cells:
|
||||
# {"r0c0": {...}, ... "rncn": {...}}
|
||||
# value: common value for all cells
|
||||
"""
|
||||
attributes = self.get_attributes(kwarg)
|
||||
|
||||
starts_r, starts_c = self.position(starts)
|
||||
ends_r, ends_c = None, None
|
||||
if ends is not None:
|
||||
ends_r, ends_c = self.position(ends)
|
||||
|
||||
if isinstance(cells, dict):
|
||||
for key, data in cells.iteritems():
|
||||
r, c = self.position(key)
|
||||
key = "r%sc%s" % (r+starts_r, c+starts_c)
|
||||
active, onchange, readonly, cell_value = \
|
||||
self.get_cell_arguments(data, default=kwarg)
|
||||
if value is None:
|
||||
v = cell_value
|
||||
else:
|
||||
v = value
|
||||
if (ends is None) or ((ends_r >= r+starts_r) and\
|
||||
(ends_c >= c+starts_c)):
|
||||
self.cell(key, v, active=active,
|
||||
readonly=readonly,
|
||||
onchange=onchange, **attributes)
|
||||
else:
|
||||
active, onchange, readonly, all_value = \
|
||||
self.get_cell_arguments(kwarg)
|
||||
for r, row in enumerate(cells):
|
||||
for c, cell_value in enumerate(row):
|
||||
if value is None:
|
||||
v = cell_value
|
||||
else:
|
||||
v = value
|
||||
key = "r%sc%s" % (r+starts_r, c+starts_c)
|
||||
if (ends is None) or \
|
||||
((ends_r >= r+starts_r) and\
|
||||
(ends_c >= c+starts_c)):
|
||||
self.cell(key, v,
|
||||
active=active,
|
||||
onchange=onchange,
|
||||
readonly=readonly,
|
||||
**attributes)
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
key = str(key)
|
||||
value = str(value)
|
||||
@@ -240,19 +773,103 @@ class Sheet:
|
||||
if node.value[:1] != '=' or not node.active:
|
||||
node.value = d[key]
|
||||
|
||||
def xml(self):
|
||||
def sheet(self):
|
||||
import gluon.html
|
||||
(DIV, TABLE, TR, TD, TH, BR) = \
|
||||
(DIV, TABLE, TR, TD, TH, BR, SCRIPT) = \
|
||||
(gluon.html.DIV, gluon.html.TABLE, gluon.html.TR, gluon.html.TD,
|
||||
gluon.html.TH, gluon.html.BR)
|
||||
gluon.html.TH, gluon.html.BR, gluon.html.SCRIPT)
|
||||
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)]),
|
||||
|
||||
header = TR(TH(), *[TH('c%s' % c) \
|
||||
for c in range(self.cols)])
|
||||
rows = []
|
||||
for r in range(self.rows):
|
||||
tds = [TH('r%s' % r),]
|
||||
for c in range(self.cols):
|
||||
key = 'r%sc%s'%(r, c)
|
||||
attributes = {"_class": "w2p_spreadsheet_col_%s" % \
|
||||
self.even_or_odd(c)}
|
||||
if key in self.td_attributes:
|
||||
attributes.update(self.td_attributes[key])
|
||||
td = TD(self.nodes[key], **attributes)
|
||||
tds.append(td)
|
||||
attributes = {"_class": "w2p_spreadsheet_row_%s" % \
|
||||
self.even_or_odd(r)}
|
||||
if str(r) in self.tr_attributes:
|
||||
attributes.update(self.tr_attributes[str(r)])
|
||||
rows.append(TR(*tds, **attributes))
|
||||
|
||||
attributes = {"_class": "w2p_spreadsheet"}
|
||||
attributes.update(self.attributes)
|
||||
|
||||
table = TABLE(header, *rows, **self.attributes)
|
||||
|
||||
if len(self.client["cells"]) >= 1:
|
||||
data = SCRIPT(\
|
||||
"""
|
||||
var w2p_spreadsheet_data = %(data)s;
|
||||
var w2p_spreadsheet_update_button = "%(update_button)s";
|
||||
var w2p_spreadsheet_update_result = null;
|
||||
function w2p_spreadsheet_update_cell(a){
|
||||
// update data
|
||||
w2p_spreadsheet_data.modified[this.id] = this.value;
|
||||
}
|
||||
function w2p_spreadsheet_update_db_callback(result){
|
||||
w2p_spreadsheet_update_result = result;
|
||||
eval(w2p_spreadsheet_update_result.db_callback);
|
||||
}
|
||||
function w2p_spreadsheet_update_db(){
|
||||
// ajax background db update
|
||||
jQuery.ajax({url: "%(url)s/data",
|
||||
type: "POST",
|
||||
data:
|
||||
{data: JSON.stringify(w2p_spreadsheet_data)},
|
||||
dataType: "json",
|
||||
success: w2p_spreadsheet_update_db_callback
|
||||
});
|
||||
}
|
||||
// add onchange cell update event
|
||||
jQuery(function(){
|
||||
jQuery(".%(name)s input").change(w2p_spreadsheet_update_cell);
|
||||
});
|
||||
|
||||
if (w2p_spreadsheet_update_button != ""){
|
||||
jQuery(function(){
|
||||
jQuery("#" + w2p_spreadsheet_update_button).click(w2p_spreadsheet_update_db);
|
||||
});
|
||||
}
|
||||
""" % dict(data=simplejson.dumps(self.client),
|
||||
name=attributes["_class"],
|
||||
url=self.url,
|
||||
update_button=self.update_button))
|
||||
|
||||
# extra row for fieldnames
|
||||
unsorted_headers = []
|
||||
if self.client["headers"] is not None:
|
||||
for fieldname, name in self.client["headers"].iteritems():
|
||||
unsorted_headers.append((self.client["columns"][fieldname],
|
||||
name))
|
||||
else:
|
||||
for fieldname, c in self.client["columns"].iteritems():
|
||||
unsorted_headers.append((c, fieldname))
|
||||
|
||||
sorted_headers = [TH(),] + \
|
||||
[TH(header[1]) for header in sorted(unsorted_headers)]
|
||||
table.insert(0, TR(*sorted_headers, _class="%s_fieldnames" % \
|
||||
attributes["_class"]))
|
||||
|
||||
else:
|
||||
data = SCRIPT(""" // web2py Spreadsheets: no db data.""")
|
||||
|
||||
return DIV(table,
|
||||
BR(),
|
||||
TABLE(*[TR(TH(key), TD(self.nodes[key])) \
|
||||
for key in self.nodes if not regex.match(key)])).xml()
|
||||
for key in self.nodes if not regex.match(key)]),
|
||||
data, **attributes)
|
||||
|
||||
def xml(self):
|
||||
return self.sheet().xml()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
s = Sheet(0, 0)
|
||||
@@ -261,7 +878,3 @@ if __name__ == '__main__':
|
||||
s.cell('c', value="=cos(a)**2+b*b")
|
||||
print s['c'].computed_value
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user