source: OpenRLabs-Git/deploy/rlabs-docker/web2py-rlabs/gluon/contrib/spreadsheet.py

main
Last change on this file was 42bd667, checked in by David Fuertes <dfuertes@…>, 4 years ago

Historial Limpio

  • Property mode set to 100755
File size: 31.5 KB
Line 
1# -*- coding: utf-8 -*-
2
3"""
4Developed by Massimo Di Pierro, optional component of web2py, BSDv3 license.
5"""
6from __future__ import print_function
7
8import re
9import pickle
10import copy
11import json
12
13
14def quote(text):
15    return str(text).replace('\\', '\\\\').replace("'", "\\'")
16
17
18class Node:
19    def __init__(self, name, value, url='.', readonly=False, active=True,
20                 onchange=None, select=False, size=4, **kwarg):
21        self.url = url
22        self.name = name
23        self.value = str(value)
24        self.computed_value = ''
25        self.incoming = {}
26        self.outcoming = {}
27        self.readonly = readonly
28        self.active = active
29        self.onchange = onchange
30        self.size = size
31        self.locked = False
32        self.select = value if select and not isinstance(value, str) else False
33
34    def xml(self):
35        if self.select:
36            selectAttributes = dict(_name=self.name,_id=self.name,_size=self.size,
37                                    _onblur="ajax('%s/blur',['%s']);"%(self.url,self.name))
38            #                        _onkeyup="ajax('%s/keyup',['%s'], ':eval');"%(self.url,self.name),
39            #                        _onfocus="ajax('%s/focus',['%s'], ':eval');"%(self.url,self.name),
40            for k,v in selectAttributes.items():
41                self.select[k] = v
42            return self.select.xml()
43        else:
44            return """<input name="%s" id="%s" value="%s" size="%s"
45        onkeyup="ajax('%s/keyup',['%s'], ':eval');"
46        onfocus="ajax('%s/focus',['%s'], ':eval');"
47        onblur="ajax('%s/blur',['%s'], ':eval');" %s/>
48        """ % (self.name, self.name, self.computed_value, self.size,
49               self.url, self.name, self.url, self.name, self.url, self.name,
50               (self.readonly and 'readonly ') or '')
51
52    def __repr__(self):
53        return '%s:%s' % (self.name, self.computed_value)
54
55
56class Sheet:
57    """
58    Basic class for creating web spreadsheets
59
60    New features:
61
62    -dal spreadsheets:
63        It receives a Rows object instance and presents
64    the selected data in a cell per field basis (table rows
65    are sheet rows as well)
66    Input should be short extension data as numeric data
67    or math expressions but can be anything supported by
68    unicode.
69
70    -row(), column() and matrix() class methods:
71        These new methods allow to set bulk data sets
72    without calling .cell() for each node
73    Example::
74
75         # controller
76         from gluon.contrib.spreadsheet import Sheet
77
78         def callback():
79             return cache.ram('sheet1', lambda: None, None).process(request)
80
81         def index():
82             # standard spreadsheet method
83             sheet = cache.ram('sheet1',
84                 lambda: Sheet(10, 10, URL(r=request, f='callback')), 0)
85             #sheet.cell('r0c3', value='=r0c0+r0c1+r0c2', readonly=True)
86             return dict(sheet=sheet)
87
88         def index():
89             # database spreadsheet method
90             sheet = cache.ram('sheet1',
91                 lambda: Sheet(10, 10, URL(r=request, f='callback'),
92                 data=db(db.mydata).select()), 0)
93             return dict(sheet=sheet)
94
95         # view
96         {{extend 'layout.html'}}
97         {{=sheet}}
98
99         or insert invidivual cells via
100
101         {{=sheet.nodes['r0c0']}}
102
103    Sheet stores a JavaScript w2p_spreadsheet_data object
104    for retrieving data updates from the client.
105
106    The data structure of the js object is as follows:
107        # columns: a dict with colname, column index map
108        # colnames: a dict with column index, colname map
109        # id_columns: list with id columns
110        # id_colnames: dict with id colname: column index map
111        # cells: dict of "rncn": value pairs
112        # modified: dict of modified cells for client-side
113
114    Also, there is a class method Sheet.update(data) that
115    processes the json data as sent by the client and
116    returns a set of db modifications (see the method help for
117    more details)
118
119    client JavaScript objects:
120
121    -var w2p_spreadsheet_data
122    Stores cell updates by key and
123    Used for updated cells control
124
125    -var w2p_spreadsheet_update_button
126    Stores the id of the update command
127    Used for event binding (update click)
128
129    var w2p_spreadsheet_update_result
130      object attributes:
131        modified - n updated records
132        errors - n errors
133        message - a message for feedback and errors
134
135    Stores the ajax db update call returned stats
136    and the db_callback string js
137    Used after calling w2p_spreadsheet_update_db()
138
139    -function w2p_spreadsheet_update_cell(a)
140    Used for responding to normal cell events
141    (encapsulates old behavior)
142
143    -function w2p_spreadsheet_update_db_callback(result)
144    Called after a background db update
145
146    -function w2p_spreadsheet_update_db()
147    Called for updating the database with
148    client data
149
150    First method: Sending data trough a form helper:
151    (the data payload must be inserted in a form field before
152    submission)
153
154    -Applying db changes made client-side
155
156    Example controller:
157    ...
158    # present a submit button with the spreadsheet
159    form = SQLFORM.factory(Field("<name>",
160                                 "text",
161                                 readable=False, writable=False,
162                                 formname="<formname>"))
163    # submit button label
164    form.elements("input [type=submit]").attributes["_value"] = \
165    T("Update database")
166    form.elements("textarea")[0].attributes["_style"] = "display: none;"
167
168    w2p_spreadsheet_update_script = SCRIPT('''
169      jQuery(
170        function(){
171          jQuery("<formname>").submit(function(){
172            jQuery("[name=<name>]").val(JSON.stringify(
173              w2p_spreadsheet_data)
174              );
175          });
176        }
177      );
178    ''')
179
180    # retrieve changes
181    if form.process().accepted:
182        data = form.vars.<name>
183        changes = Sheet.updated(data)
184
185        # Do db I/O:
186        for table, rows in changes.iteritems():
187            for row, values in rows.iteritems():
188                db[table][row].update_record(**values)
189
190    # the action view should expose {{=form}}, {{=sheet}}, {{=myscript}}
191    return dict(form=form, sheet=sheet,
192                myscript=w2p_spreadseet_update_script)
193
194    Second method: Sending data updates with .ajax()
195
196    -spreadsheet page's view:
197
198    {{
199    =INPUT(_type="button", _value="update data",
200             _id="w2p_spreadsheet_update_data")
201    }}
202
203    {{=SCRIPT('''
204    jQuery(function(){
205    jQuery("#w2p_spreadsheet_update_data").click(
206        function(){
207          jQuery.ajax({url: "%s",
208                    type: "POST",
209                    data:
210                      {data:
211                        JSON.stringify(w2p_spreadsheet_data)}
212                    }
213          );
214        }
215    );
216    });
217    ''' % URL(c="default", f="modified"))}}
218
219    -controller: modified function
220    def modified():
221        data = request.vars.data
222        changes = Sheet.updated(data)
223        # (for db I/O see first method)
224        return "ok"
225
226
227    Third method:
228    When creating a Sheet instance, pass a kwarg update_button=<button id>
229    This step will auto process the updated data with db I/O (requires calling
230    .process() with db=<DAL instance>). You must expose an html element which
231    supports the .click() event, i.e. a normal button.
232
233    # TODO:
234    # -¿SIGNED URLS?
235    # -Delete checkbox columns for each table and default
236    # -Deletable=True option for showing/hiding delete checkboxes
237    # -process() method support for db I/O
238
239    """
240
241    regex = re.compile('(?<!\w)[a-zA-Z_]\w*')
242    pregex = re.compile('\d+')
243    re_strings = re.compile(r'(?P<name>'
244                            + r"[uU]?[rR]?'''([^']+|'{1,2}(?!'))*'''|"
245                            + r"'([^'\\]|\\.)*'|"
246                            + r'"""([^"]|"{1,2}(?!"))*"""|'
247                            + r'"([^"\\]|\\.)*")', re.DOTALL)
248
249    def dumps(self):
250        dump = pickle.dumps(self)
251        return dump
252
253    @staticmethod
254    def position(key):
255        """ Returns int row, int col for a 'rncn' formatted key'"""
256        try:
257            r, c = Sheet.pregex.findall(key)
258            r, c = int(r), int(c)
259        except (ValueError, IndexError, TypeError) as e:
260            error = "%s. %s" % \
261                ("Unexpected position parameter",
262                 "Must be a key of type 'rncn'")
263            raise ValueError(error)
264        return r, c
265
266    @staticmethod
267    def loads(data):
268        sheet = pickle.loads(data)
269        return sheet
270
271    @staticmethod
272    def updated(data):
273        """ Reads spreadsheet update information sent client-side.
274
275        Returns a dict with updated database rows/fields.
276        Structure:
277        {<tablename>:{
278                    <id>:{<fieldname>:<new value>,
279                            <fieldname>:<new value>,
280                            ...
281                    },
282                    ...
283                    }
284        }
285
286        data dict argument:
287
288        # columns: (a dict with colname, column index map)
289        # colnames: (a dict with column index, colname map)
290        # id_columns: list with id columns
291        # id_colnames: dict with id colname: column index map
292        # cells: dict of "rncn": value pairs
293        # modified: dict of modified cells for client-side
294
295        """
296        data = json.loads(data)
297
298        # record update dict
299        changes = {}
300
301        # read column index per table
302        # table, id column map
303        tablenames = {}
304        for colname, i in data["id_colnames"].iteritems():
305            tablenames[colname.split(".")[0]] = i
306
307        # Separate multitable rows
308        # Identify each row id (old data)
309        # Build a dict with table/row/field
310        # update information.
311
312        # collect new data by row (modified):
313        for key, value in data["modified"].iteritems():
314            r, c = Sheet.position(key)
315
316            # don't apply repeated values
317            if data["cells"][key] != value:
318                # read tablename
319                tablename, fieldname = data["colnames"][str(c)].split(".")
320
321                # read db record id
322                row_id_key = "r%sc%s" % (r, tablenames[tablename])
323                row_id = data["cells"][row_id_key]
324
325                changes.setdefault(tablename, {})
326                changes[tablename].setdefault(row_id, {})
327                changes[tablename][row_id][fieldname] = value
328
329        return changes
330
331    def process(self, request, db=None, db_callback=None):
332        """
333        call this in action that creates table, it will handle ajax callbacks
334        optional db (a DAL instance). It's required for db I/O
335        optional callback string. js commands to call after successful
336        ajax db update.
337        db_callback string format keys:
338          modified (number of rows updated)
339        """
340
341        if not request.args(0) == "data":
342            # normal cell processing
343            cell = request.vars.keys()[0]
344
345            if request.args(0) == 'focus':
346                return "jQuery('#%(cell)s').val('%(value)s');" % \
347                    dict(cell=cell, value=quote(self[cell].value))
348
349            value = request.vars[cell]
350            self[cell] = value
351
352            if request.args(0) == 'blur':
353                return "jQuery('#%(cell)s').val('%(value)s');" % \
354                    dict(cell=cell, value=quote(self[cell].computed_value))
355
356            elif request.args(0) == 'keyup':
357                jquery = ''
358                for other_key in self.modified:
359                    if other_key != cell:
360                        jquery += "jQuery('#%(other_key)s').val('%(value)s');" % \
361                            dict(other_key=other_key,
362                                 value=quote(self[other_key].computed_value))
363
364        else:
365            # spreadsheet db update
366            result = dict(modified=0,
367                          errors=0,
368                          message="",
369                          db_callback="")
370
371            if db is not None:
372                data = request.vars["data"]
373                changes = self.updated(data)
374
375                # Do db I/O:
376                for table, rows in changes.iteritems():
377                    for row, values in rows.iteritems():
378                        db[table][row].update_record(**values)
379                        result["modified"] += 1
380
381                if db_callback is not None:
382                    result["db_callback"] = db_callback
383            else:
384                result["message"] = "Sheet.process Error. No db found."
385            return json.dumps(result)
386
387        return jquery
388
389    def get_attributes(self, data):
390        attributes = {}
391        for k in data.keys():
392            if k.startswith("_"):
393                attributes[k] = data[k]
394        return attributes
395
396    def even_or_odd(self, v):
397        """ Used for table row stripping """
398        if v % 2 == 0:
399            return "even"
400        else:
401            return "odd"
402
403    def __init__(self, rows, cols, url='.', readonly=False,
404                 active=True, onchange=None, value=None, data=None,
405                 headers=None, update_button="", c_headers=None,
406                 r_headers=None, **kwarg):
407
408        """
409        Arguments:
410        headers: a dict with "table.fieldname": name values
411        value: common value for all spreadsheet
412        (can be a lambda x, y: z or function reference)
413
414        Rows and cols values will be updated automatically to fit
415        the data boundaries when the data argument is a Rows object.
416
417        self.client: for storing sheet data client side
418        columns: a dict with colname, column index map
419        colnames: a dict with column index, colname map
420        id_columns: list with id columns
421        id_colnames: dict with id colname: column index map
422        cells: dict of "rncn": value pairs
423        modified: dict of modified cells for client-side
424        edition.
425        """
426
427        self.rows = rows
428        self.cols = cols
429        self.url = url
430        self.nodes = {}
431        self.error = 'ERROR: %(error)s'
432        self.allowed_keywords = ['for', 'in', 'if', 'else', 'and', 'or', 'not',
433                                 'i', 'j', 'k', 'x', 'y', 'z', 'sum']
434        self.value = value
435        self.environment = {}
436        self.attributes = self.get_attributes(kwarg)
437        self.tr_attributes = {}
438        self.td_attributes = {}
439
440        self.c_headers = c_headers
441        self.r_headers = r_headers
442
443        self.data = data
444        self.readonly = readonly
445
446        self.update_button = update_button
447
448        self.client = {
449            "columns": {},
450            "colnames": {},
451            "id_columns": [],
452            "id_colnames": {},
453            "cells": {},
454            "modified": {},
455            "headers": headers
456        }
457
458        # if db and query:
459        if self.data is not None:
460            # retrieve row columns length
461            self.rows = len(self.data)
462            # retrieve rows length
463            self.cols = len(self.data.colnames)
464
465            # map row data to rncn values
466            for x, colname in enumerate(self.data.colnames):
467                self.client["columns"][colname] = x
468                self.client["colnames"][x] = colname
469
470            for x, row in enumerate(self.data):
471                for colname, y in self.client["columns"].iteritems():
472                    key = "r%sc%s" % (x, y)
473                    tablename, fieldname = colname.split(".")
474                    try:
475                        value = row[tablename][fieldname]
476                    except (KeyError, AttributeError):
477                        # single table query
478                        value = row[fieldname]
479                    self.client["cells"][key] = str(value)
480                    # TODO: support different id keys
481                    if ".id" in colname:
482                        self.client["id_columns"].append(y)
483                        self.client["id_colnames"][colname] = y
484
485        for k in xrange(self.rows * self.cols):
486            key = 'r%sc%s' % (k / self.cols, k % self.cols)
487            r, c = self.position(key)
488            if key in self.client["cells"]:
489                value = self.client["cells"][key]
490                # readonly id values
491                if c in self.client["id_columns"]:
492                    readonly = True
493                else:
494                    readonly = self.readonly
495            elif self.value is not None:
496                if callable(self.value):
497                    value = self.value(r, c)
498                else:
499                    value = self.value
500            else:
501                value = '0.00'
502            self.cell(key, value,
503                      readonly, active, onchange)
504
505        exec('from math import *', {}, self.environment)
506
507    def delete_from(self, other_list):
508        indices = [k for (k, node) in enumerate(other_list) if k == node]
509        if indices:
510            del other_list[indices[0]]
511
512    def changed(self, node, changed_nodes=[]):
513        for other_node in node.outcoming:
514            if not other_node in changed_nodes:
515                changed_nodes.append(other_node)
516                self.changed(other_node, changed_nodes)
517        return changed_nodes
518
519    def define(self, name, obj):
520        self.environment[name] = obj
521
522    def cell(self, key, value, readonly=False, active=True,
523             onchange=None, select=False, **kwarg):
524        """
525        key is the name of the cell
526        value is the initial value of the cell. It can be a formula "=1+3"
527        a cell is active if it evaluates formulas
528
529        Value can be a function(r, c) which returns a string
530        """
531
532        if not self.regex.match(key):
533            raise SyntaxError("Invalid cell name: %s" % key)
534        else:
535            attributes = self.get_attributes(kwarg)
536            if attributes is not None:
537                self.td_attributes[key] = attributes
538
539        key = str(key)
540        r, c = self.position(key)
541
542        if callable(value):
543            value = value(r, c)
544
545        node = Node(key, value, self.url, readonly, active,
546                    onchange, select=select, **kwarg)
547        self.nodes[key] = node
548        self[key] = value
549
550    def get_cell_arguments(self, data, default=None):
551        """Reads cell arguments from a dict object"""
552        active = True
553        onchange = None
554        readonly = False
555        value = ""
556        if default is not None:
557            data.update(default)
558        if "active" in data:
559            active = data["active"]
560        if "readonly" in data:
561            readonly = data["readonly"]
562        if "onchange" in data:
563            onchange = data["onchange"]
564        if "value" in data:
565            value = data["value"]
566        return active, onchange, readonly, value
567
568    def row(self, row, cells, value=None, **kwarg):
569        # row: row index (0, 1, ...)
570        # cells: a sequence of values or a dict of dict with
571        # arg: value pairs
572        # one column example:
573        # {"0": {"value":1.0, "readonly":False, "active":True, "onchange":None}}
574        # value: common value for all cells
575
576        attributes = self.get_attributes(kwarg)
577        if attributes is not None:
578            self.tr_attributes[str(row)] = attributes
579        if isinstance(cells, dict):
580            for col, data in cells.iteritems():
581                key = "r%sc%s" % (row, col)
582                active, onchange, readonly, cell_value = \
583                    self.get_cell_arguments(data, default=kwarg)
584                if value is None:
585                    v = cell_value
586                else:
587                    v = value
588                self.cell(key, v, active=active,
589                          readonly=readonly,
590                          onchange=onchange, **attributes)
591        else:
592            active, onchange, readonly, all_value = \
593                self.get_cell_arguments(kwarg)
594            for col, cell_value in enumerate(cells):
595                key = "r%sc%s" % (row, col)
596                if value is None:
597                    v = cell_value
598                else:
599                    v = value
600                self.cell(key, v, active=active,
601                          onchange=onchange,
602                          readonly=readonly, **attributes)
603
604    def column(self, col, cells, value=None, **kwarg):
605        """
606        # col: column index (0, 1, ...)
607        # cells: a sequence of values or a dict of dict with
608        # arg: value pairs
609        # one row example:
610        # {"0": {"value":1.0, "readonly":False, "active":True, "onchange":None}}
611        # value: common value for all cells
612        """
613        attributes = self.get_attributes(kwarg)
614
615        if isinstance(cells, dict):
616            for row, data in cells.iteritems():
617                key = "r%sc%s" % (row, col)
618                active, onchange, readonly, cell_value = \
619                    self.get_cell_arguments(data, default=kwarg)
620                if value is None:
621                    v = cell_value
622                else:
623                    v = value
624                self.cell(key, v, active=active, readonly=readonly,
625                          onchange=onchange, **attributes)
626        else:
627            active, onchange, readonly, all_value = \
628                self.get_cell_arguments(kwarg)
629            for row, cell_value in enumerate(cells):
630                key = "r%sc%s" % (row, col)
631                if value is None:
632                    v = cell_value
633                else:
634                    v = value
635                self.cell(key, v, active=active,
636                          onchange=onchange, readonly=readonly,
637                          **attributes)
638
639    def matrix(self, cells, starts="r0c0", ends=None, value=None, **kwarg):
640        """
641        Insert a n x n matrix or a set of cells
642        # starts: upper left cell
643        # ends: lower right cell
644
645        # cells: a sequence of value sequences
646        # or a dict with "rncn" keys
647        # Example 1 cells:
648        # ((v11, v12, ... v1n),
649           (vn2, vn2, ... vnn))
650        # Example 2 cells:
651        # {"r0c0": {...}, ... "rncn": {...}}
652        # value: common value for all cells
653        """
654        attributes = self.get_attributes(kwarg)
655
656        starts_r, starts_c = self.position(starts)
657        ends_r, ends_c = None, None
658        if ends is not None:
659            ends_r, ends_c = self.position(ends)
660
661        if isinstance(cells, dict):
662            for key, data in cells.iteritems():
663                r, c = self.position(key)
664                key = "r%sc%s" % (r + starts_r, c + starts_c)
665                active, onchange, readonly, cell_value = \
666                    self.get_cell_arguments(data, default=kwarg)
667                if value is None:
668                    v = cell_value
669                else:
670                    v = value
671                if (ends is None) or ((ends_r >= r + starts_r) and
672                                      (ends_c >= c + starts_c)):
673                    self.cell(key, v, active=active,
674                              readonly=readonly,
675                              onchange=onchange, **attributes)
676        else:
677            active, onchange, readonly, all_value = \
678                self.get_cell_arguments(kwarg)
679            for r, row in enumerate(cells):
680                for c, cell_value in enumerate(row):
681                    if value is None:
682                        v = cell_value
683                    else:
684                        v = value
685                    key = "r%sc%s" % (r + starts_r, c + starts_c)
686                    if (ends is None) or \
687                       ((ends_r >= r + starts_r) and
688                            (ends_c >= c + starts_c)):
689                        self.cell(key, v,
690                                  active=active,
691                                  onchange=onchange,
692                                  readonly=readonly,
693                                  **attributes)
694
695    def __setitem__(self, key, value):
696        key = str(key)
697        value = str(value)
698        node = self.nodes[key]
699        node.value = value
700        if value[:1] == '=' and node.active:
701            # clear all edges involving current node
702            for other_node in node.incoming:
703                del other_node.outcoming[node]
704            node.incoming.clear()
705            # build new edges
706            command = self.re_strings.sub("''", value[1:])
707            node.locked = False
708            for match in self.regex.finditer(command):
709                other_key = match.group()
710                if other_key == key:
711                    self.computed_value = self.error % dict(error='cycle')
712                    self.modified = {}
713                    break
714                if other_key in self.nodes:
715                    other_node = self.nodes[other_key]
716                    other_node.outcoming[node] = True
717                    node.incoming[other_node] = True
718                elif not other_key in self.allowed_keywords and \
719                        not other_key in self.environment:
720                    node.locked = True
721                    node.computed_value = \
722                        self.error % dict(
723                            error='invalid keyword: ' + other_key)
724                    self.modified = {}
725                    break
726            self.compute(node)
727        else:
728            try:
729                node.computed_value = int(node.value)
730            except:
731                try:
732                    node.computed_value = float(node.value)
733                except:
734                    node.computed_value = node.value
735            self.environment[key] = node.computed_value
736            if node.onchange:
737                node.onchange(node)
738        self.modified = self.iterate(node)
739
740    def compute(self, node):
741        if node.value[:1] == '=' and not node.locked:
742            try:
743                exec('__value__=' + node.value[1:], {}, self.environment)
744                node.computed_value = self.environment['__value__']
745                del self.environment['__value__']
746            except Exception as e:
747                node.computed_value = self.error % dict(error=str(e))
748        self.environment[node.name] = node.computed_value
749        if node.onchange:
750            node.onchange(node)
751
752    def iterate(self, node):
753        output = {node.name: node.computed_value}
754        changed_nodes = self.changed(node)
755        while changed_nodes:
756            ok = False
757            set_changed_nodes = set(changed_nodes)
758            for (k, other_node) in enumerate(changed_nodes):
759                #print other_node, changed_nodes
760                if not set(other_node.incoming.keys()).\
761                        intersection(set_changed_nodes):
762                    #print 'ok'
763                    self.compute(other_node)
764                    output[other_node.name] = other_node.computed_value
765                    #print other_node
766                    del changed_nodes[k]
767                    ok = True
768                    break
769            if not ok:
770                return {}
771        return output
772
773    def __getitem__(self, key):
774        return self.nodes[str(key)]
775
776    def get_computed_values(self):
777        d = {}
778        for key in self.nodes:
779            node = self.nodes[key]
780            if node.value[:1] != '=' or not node.active:
781                d[key] = node.computed_value
782        return d
783
784    def set_computed_values(self, d):
785        for key in d:
786            if not key in self.nodes:
787                continue
788            node = self.nodes[key]
789            if node.value[:1] != '=' or not node.active:
790                node.value = d[key]
791
792    def sheet(self):
793        import gluon.html
794        (DIV, TABLE, TR, TD, TH, BR, SCRIPT) = \
795            (gluon.html.DIV, gluon.html.TABLE, gluon.html.TR, gluon.html.TD,
796             gluon.html.TH, gluon.html.BR, gluon.html.SCRIPT)
797        regex = re.compile('r\d+c\d+')
798
799        if not self.c_headers:
800            header = TR(TH(), *[TH('c%s' % c)
801                            for c in range(self.cols)])
802        else:
803            header = TR(TH(), *[TH('%s' % c)
804                            for c in self.c_headers])
805
806        rows = []
807        for r in range(self.rows):
808            if not self.r_headers:
809                tds = [TH('r%s' % r), ]
810            else:
811                tds = [TH('%s' % self.r_headers[r]), ]
812            for c in range(self.cols):
813                key = 'r%sc%s' % (r, c)
814                attributes = {"_class": "w2p_spreadsheet_col_%s" %
815                              self.even_or_odd(c)}
816                if key in self.td_attributes:
817                    attributes.update(self.td_attributes[key])
818                td = TD(self.nodes[key], **attributes)
819                tds.append(td)
820            attributes = {"_class": "w2p_spreadsheet_row_%s" %
821                          self.even_or_odd(r)}
822            if str(r) in self.tr_attributes:
823                attributes.update(self.tr_attributes[str(r)])
824            rows.append(TR(*tds, **attributes))
825
826        attributes = {"_class": "w2p_spreadsheet"}
827        attributes.update(self.attributes)
828
829        table = TABLE(header, *rows, **self.attributes)
830
831        if len(self.client["cells"]) >= 1:
832            data = SCRIPT(
833                """
834            var w2p_spreadsheet_data = %(data)s;
835            var w2p_spreadsheet_update_button = "%(update_button)s";
836            var w2p_spreadsheet_update_result = null;
837            function w2p_spreadsheet_update_cell(a){
838              // update data
839              w2p_spreadsheet_data.modified[this.id] = this.value;
840            }
841            function w2p_spreadsheet_update_db_callback(result){
842              w2p_spreadsheet_update_result = result;
843              eval(w2p_spreadsheet_update_result.db_callback);
844            }
845            function w2p_spreadsheet_update_db(){
846              // ajax background db update
847              jQuery.ajax({url: "%(url)s/data",
848                           type: "POST",
849                           data:
850                             {data: JSON.stringify(w2p_spreadsheet_data)},
851                           dataType: "json",
852                           success: w2p_spreadsheet_update_db_callback
853                           });
854            }
855            // add onchange cell update event
856            jQuery(function(){
857              jQuery(".%(name)s input").change(w2p_spreadsheet_update_cell);
858            });
859
860            if (w2p_spreadsheet_update_button != ""){
861              jQuery(function(){
862                jQuery("#" + w2p_spreadsheet_update_button).click(
863                    w2p_spreadsheet_update_db);
864              });
865            }
866            """ % dict(data=json.dumps(self.client),
867                       name=attributes["_class"],
868                       url=self.url,
869                       update_button=self.update_button))
870
871            # extra row for fieldnames
872            unsorted_headers = []
873            if self.client["headers"] is not None:
874                for fieldname, name in self.client["headers"].iteritems():
875                    unsorted_headers.append((self.client["columns"][fieldname],
876                                             name))
877            else:
878                for fieldname, c in self.client["columns"].iteritems():
879                    unsorted_headers.append((c, fieldname))
880
881            sorted_headers = [TH(), ] + \
882                [TH(header[1]) for header in sorted(unsorted_headers)]
883            table.insert(0, TR(*sorted_headers,
884                                **{'_class': "%s_fieldnames" %
885                                   attributes["_class"]}))
886        else:
887            data = SCRIPT(""" // web2py Spreadsheets: no db data.""")
888
889        return DIV(table,
890                   BR(),
891                   TABLE(*[TR(TH(key), TD(self.nodes[key]))
892                           for key in self.nodes if not regex.match(key)]),
893                   data, **attributes)
894
895    def xml(self):
896        return self.sheet().xml()
897
898
899if __name__ == '__main__':
900    s = Sheet(0, 0)
901    s.cell('a', value="2")
902    s.cell('b', value="=sin(a)")
903    s.cell('c', value="=cos(a)**2+b*b")
904    print(s['c'].computed_value)
Note: See TracBrowser for help on using the repository browser.