内容


利用 Django 与 jQuery 来创建电子表格应用程序

Comments

本文描述了如何利用 jQuery、jQuery 插件、以及 Django 来实现基于 web 的电子表格。并不是为了与 Google Docs 进行竞争,而是要演示如果创建 “office” 风格的应用程序,并给出大量可用的 jQuery 插件与工具。我采用 SQLite/Python/Django 栈作为后端,您也可以通过很小的工作量,来实现到其他框架的端口,比如 Ruby on Rails。

项目依赖项

本文采用如下 Python 技术(见 参考资料 中的链接):

  • Python 2.5+
  • simplejson
  • Django 1.2.3

注意: Python 2.5 不包括 simplejson,但在 Python 后续的版本中包含。

要想避免获取所有 jQuery 依赖项过程中的麻烦,可通过 参考资料 中的链接来下载完整的演示。在前端,需要如下技术:

  • jQuery 1.4.3
  • jQuery UI 1.8.5
  • SlickGrid
  • jQuery JSON

所有的第三方库能处理大部分的工作量,特别是 SlickGrid。我选择 SlickGrid 是因为,它支持突出显示/选择单元格组 — 适用于优化单元格的数学操作与分析功能。它还支持在滚动时加载数据。还有很多优秀的 jQuery 网格插件可供使用,包括 Flexigrid、jQuery Grid、jqGridView、以及 Ingrid。此外,jQuery 项目已宣布提供官方 jQuery Grid 插件的计划。

电子表格规范

每个电子表格都包含单个工作簿,每个工作簿包含一个或多个数据表。当首次输入的字符是等号(=)时,表中的每个单元格应当执行算术运算。否则,输入的文本应保持原样。数据加载到 JSON 对象中,异步发送给后端,并保存到数据库中。电子表格将处理 Open、New、以及 Save 操作,并且工作簿的名称将出现在顶部的可编辑文本框内。

单击 Open 打开一个 jQuery UI 窗口,显示数据库中的现有工作簿。选择工作簿后,利用 Asynchronous JavaScript and XML(Ajax)来检索所存储的 JSON 数据,并呈现给网格。异步地将以 JSON 格式发送的网格数据存储到后端。New 操作会清除所有引用,并重新加载干净的工作簿。

最后,工作簿的表格将被分成不重复的 jQuery 选项卡。同任何其他电子表格一样,选项卡将在底部展示,并通过单击底部的按钮来动态增加。

项目结构

将所有的 CSS/JavaScript/images 放置到项目顶级目录中的 resources 文件夹中。Django 应用程序将包含名为 index.html 的模板,那只是一些标记,用于保持 HTML 语义与 JavaScript 代码语法。组件的生成,比如网格,是动态完成的。电子表格的定义包含在文件 spreadsheet.js 中。

创建 Django 后端

首先,通过执行以下命令来创建 Django 项目:

django-admin startproject spreadsheet

然后,cd 到新创建的项目中,通过调用以下内容来创建应用程序:

django-admin startapp spreadsheet_app

本文采用 SQLite3 来避免过多的数据库相关工作,但是,您可随意选择任何关系型数据库系统(RDBS)。修改 settings.py 文件来应用清单 1 中的代码。

清单 1. Django settings.py 文件
import os
APPLICATION_DIR = os.path.dirname( globals()[ '__file__' ] )

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3', 
        'NAME': 'db',                      
        'USER': '',                      
        'PASSWORD': '',                 
        'HOST': '',                    
        'PORT': '',                   
    }
}

MEDIA_ROOT = os.path.join( APPLICATION_DIR, 'resources' )
MEDIA_URL = 'http://localhost:8000/resources/'

ROOT_URLCONF = 'spreadsheet.urls'

TEMPLATE_DIRS = (
    os.path.join( APPLICATION_DIR, 'templates' ),
)

INSTALLED_APPS = (
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.sites',
    'django.contrib.messages',
    'spreadsheet_app',
)

不必修改 settings.py 文件中的任何其他变量。现在需要配置 URL 映射。在本例中,仅需要两个映射:一个用于静态传送的文件,另一个用于指向索引。清单 2 展示了相关代码。

清单 2. urls.py 文件
from django.conf.urls.defaults import *
from django.conf import settings
import spreadsheet.spreadsheet_app.views as views

urlpatterns = patterns('',
    ( r'^resources/(?P<path>.*)$',
      'django.views.static.serve',
      { 'document_root': settings.MEDIA_ROOT } ),
    url( r'^spreadsheet_app/', views.index, name="index" ) ,
)

在项目的顶级目录中创建目录 resources,并创建 css、js、与 images 子目录。SlickGrid 之类的下载依赖项,以及用于应用程序的自定义 JavaScript 代码存储在此处。如果觉得麻烦,可以下载演示版并复制 resources 目录即可。

接下来,创建域模型(见清单 3)。该模型将仅包含 3 个字段:workbook_namesheet_namedata。 Django Object-Relational Mapper(ORM)会自动创建关键字段 id

清单 3. models.py 文件
# file: spreadsheet_app/models.py

from django.db import models

class Workbooks(models.Model):
    workbook_name = models.CharField(max_length=30)
    sheet_name = models.CharField(max_length=30)
    data = models.TextField()

其实,您并没有完全发挥 Django 的优势。您只是想在处理后端工作的同时提升 jQuery 前端的性能。

最后,创建索引视图。索引视图处理电子表格相关的创建/读取/更新操作。不必深入探讨索引视图的细节问题,清单 4 展示了如何处理进入的请求。

清单 4. 视图
# file:spreadsheet_app/views.py
from django.template.context import RequestContext
from spreadsheet.spreadsheet_app.models import Workbooks
import simplejson as json
from django.http import HttpResponse

def index(request):

    app_action = request.POST.get('app_action')
    posted_data = request.POST.get('json_data')

    if posted_data is not None and app_action == 'save':
        ...

    elif app_action == 'get_sheets':
        ...
  
    elif app_action == 'list':
        ...

import 完成后,可以看到索引视图接受了包含客户端所发送 post 数据的请求对象。可以得到两个参数:app_actionposted_dataapp_action 参数说明客户端请求什么动作,比如创建新的工作表。posted_data 参数是客户端发送的,针对单个表的 JSON 数据。不同的动作通过 if 语句来处理;可以保存表,取得工作簿的所有表,或者取得数据库中工作簿的清单。

您将在后面看到索引视图的相关细节。此时,要在 spreadsheets_app 目录中增加名为 templates 的目录。在 templates 子目录中,增加文件 index.html,这是本项目唯一的模板。清单 5 展示了相关代码。

清单 5. 索引模板
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" 
    "http://www.w3.org/TR/html4/strict.dtd">
<html>
  <head>
     <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1"i>
     <title>Overly Simple Spreadsheet</title>
     <link rel="stylesheet" href="{{MEDIA_URL}}css/smoothness/jquery-ui-1.8.5.custom.css" 
           type="text/css" media="screen" charset="utf-8" />
     <link rel="stylesheet" href="{{MEDIA_URL}}css/slick.grid.css" type="text/css" 
           media="screen" charset="utf-8" />
     <link rel="stylesheet" href="{{MEDIA_URL}}css/examples.css" type="text/css" 
           media="screen" charset="utf-8" />
     <link rel="stylesheet" href="{{MEDIA_URL}}css/spreadsheet.css" type="text/css" 
           media="screen" charset="utf-8" />

     <script type="text/javascript" src="{{MEDIA_URL}}js/jquery-1.4.3.min.js"></script>
     <script type="text/javascript" src="{{MEDIA_URL}}js/jquery.json.js"></script>
     <script type="text/javascript" src="{{MEDIA_URL}}js/jquery-ui-1.8.5.custom.min.js">
         </script>
     <script type="text/javascript" src="{{MEDIA_URL}}js/jquery.event.drag-2.0.min.js">
         </script>
     <script type="text/javascript" src="{{MEDIA_URL}}js/ui/jquery.ui.tabs.js"></script>
     <script type="text/javascript" src="{{MEDIA_URL}}js/slick.editors.js"></script>
     <script type="text/javascript" src="{{MEDIA_URL}}js/slick.grid.js"></script>
     <script type="text/javascript" src="{{MEDIA_URL}}js/spreadsheet.js"></script>

  </headi>
  <body>
  </body>
</html>

如您所见,在 HTML 中没有控制逻辑或样式 — 仅有标记。事实上,甚至主体中也没有任何元素。这些都通过 JavaScript 代码来动态生成,也就是通过方法调用来增加或删除元素。

spreadsheet.js 概览

加载完成后,电子表格呈现 UI 并加载单个选项卡。清单 6 展示了用于 render_ui 的代码。

清单 6. spreadsheet.js 中的 render_ui 方法
function render_ui(){
    insert_menu_markup();
    insert_grid_markup();
    make_grid_component();
    add_newtab_button();
    insert_open_dialog_markup();
    make_open_dialog();
}

我们来看一下每个方法的工作方式,从 insert_menu_markup 开始。该方法仅向顶部菜单增加 HTML 代码,如清单 7 所示。该菜单由三个按钮 — new、open、和 save — 以及用于展示并输入工作簿名称的文本字段组成。可采用 jQuery prepend 来确定何时增加了标记,它将作为第一个元素插入到主体中。

清单 7. 用于生成菜单标记的方法
// OK, it's not really a menu...yet :-)
function insert_menu_markup(){
    $("body").prepend(
       '<input type="text" id="workbook_name" name="workbook_name" value="">');
    $("body").prepend('<input id="save" type="button" value="save"/>');
    $("body").prepend('<input id="open" type="button" value="open"/>');
    $("body").prepend('<input id="new" type="button" value="new"/>');
}

图 1 展示了一个非常简单的菜单,它仅包含一些按钮与文本框。

图 1. 菜单
截屏包含三个选项卡:new、open、和 save,以及 invoices_2010 的名称。
截屏包含三个选项卡:new、open、和 save,以及 invoices_2010 的名称。

insert_grid_markup 方法与 insert_menu_markup 类似,只是这一次采用了 append 方法。jQuery UI 请求 <div> 中的一个清单来生成选项卡组件:

function insert_grid_markup(){
    var workbook_widget = '<div id="tabs" class="tabs-bottom"><ul><li></li></ul></div>';  
    $('body').append(workbook_widget);
}

此时,通过调用方法 make_grid_component 来使选项卡生效。清单 8 展示了相关代码。

清单 8. 将网格 div 转换为选项卡
function make_grid_component(){
    $("#tabs").tabs();
    $(".tabs-bottom .ui-tabs-nav, .tabs-bottom .ui-tabs_nav > *")
    .removeClass("ui-corner-all ui-corner-top")
    .addClass("ui-corner-bottom");
}

可采用 jQuery id 选择器来获取到选项卡 <div> 的引用,然后调用 tabs() 方法将 <div> 转换成选项卡组件。默认 CSS 类 ui-corner-top 被移除,然后增加了类 ui-corner-bottom,因此选项卡出现在底部,如图 2 所示。

图 2. 工作簿选项卡
三个选项卡名为 Sheet 0、Sheet 1、Sheet 2。

该组件是所有数据网格的容器。此时,在选项卡的下面增加按钮,它将会在每次单击时动态增加选项卡。可通过方法 add_newtab_button 来完成此任务:

function add_newtab_button(){
    $('body').append('<input id="new_tab_button" type="button" value="+"/>');
}

最后一个可视化组件是创建 Open 窗体,可通过清单 9 中展示的 insert_open_dialog_markup 方法来创建。同其他插入标记方法一样,它创建包含标记信息的字符串,并将其附加到主体中。

清单 9. 用于生成对话框标记的方法
function insert_open_dialog_markup(){
    var dlg = '<div id="dialog_form" title="Open">' +
    '<div id="dialog_form" title="Open">' +
    '<p>Select an archive.</p><form>'+
    '<select id="workbook_list" name="workbook_list">' +
    '</select></form></div>';
    $("body").append(dlg);
}

此时已有了用于窗体的标记,可通过清单 10 中展示的 make_open_dialog 方法来增加其功能 — render_ui 中的最后一个方法。通过调用 .dialog() 方法,并向其传递参数 autoOpen:false,只有当表单被明确地打开时,它才会在 web 页面中展示。对话框表单包含选择清单,该清单中包含了所加载工作簿的名称。

清单 10. 使 Open 窗体生效
function make_open_dialog(){
    $('#dialog_form').dialog({
        autoOpen: false,
        modal: true,
        buttons: {
            "OK":function(){
                selected_wb = $('option:selected').attr('value');
                $(this).dialog('close');

                // remove grid, existing forms, and recreate
                $('body').html('');
                render_ui();

                // load grids and create forms with invisible inputs
                load_sheets(selected_wb);

                // place workbook name in text field
                $('#workbook_name').val(selected_wb);
            },
            "Cancel":function(){
                $(this).dialog('close');
            }
        }
    });
}

再次使用 jQuery 选择器来对 dialog_form 进行处理并调用 dialog() 方法,该方法将 html 元素转换为 jQuery 窗体。Open 窗体是个模式,不会随着页面加载而打开。它还包含两个按钮 — OK 与 Cancel。后者的功能是关闭窗体。OK 函数在选择清单中找到所选项目,并获取其值。然后它会关闭窗体,移除主体中的任何子元素,重新呈现 GUI 组件,并加载表格(术语称为 SlickGrids)。正如前面提到的,因为标记的生成全在方法中实现,所以增加与移除这些组件已无关紧要。

呈现 UI 基础之后,现在编写方法来打开具有网格的选项卡。继续采用这种自上而下的方法,清单 11 展示了 openTab 方法,这是本应用程序的关键功能。

注意: 应用程序中的每个表都包含一个 ID,它符合一个简单的命名约定:tabs_ 后面加上表中的选项卡号。

清单 11. 向工作簿增加新选项卡的方法
function openTab(sheet_id) {
  numberOfTabs = $("#tabs").tabs("length");
  tab_name = "tabs_" + numberOfTabs;

  $("#tabs").tabs("add","#" + tab_name,"Sheet " + numberOfTabs, numberOfTabs);
  $("#" + tab_name).css("display","block");
  $("#tabs").tabs("select",numberOfTabs);

  $('#'+tab_name ).css('height','80%');
  $('#'+tab_name ).css('width','95%');
  $('#'+tab_name ).css('float','left');
  add_grid(tab_name, numberOfTabs);

  // add form for saving this tabs data
  if(!sheet_id){
   $('body').append(
   '<form method="post" action="?" id="'+tab_name +'_form" name="'+tab_name+'_form">'+
   '<input type="hidden" id="data'+numberOfTabs+'" name="data'+numberOfTabs+'" value="">'+
   '<input type="hidden" id="sheet_id" name="sheet_id" value="">' +
   '</form>');
  } else {
   $('body').append(
   '<form method="post" action="?" id="'+tab_name +'_form" name="'+tab_name+'_form">' +
  '<input type="hidden" id="data'+numberOfTabs +'" name="data'+numberOfTabs+'" value="">'+
   '<input type="hidden" id="sheet_id" name="sheet_id" value="'+sheet_id+'">' +
   '</form>');
  }
}

如果您有兴趣,那么可以选择比较优雅的方法来编写标记字符串,那就是采用一些 JavaScript 模板,而不是将字符串连接在一起。唯一的表 ID 保存在隐藏的 input 元素中。为了保存 JSON 数据,还增加了另一个隐藏元素。这一隐藏 input 遵循一个简单的命名规则 — 数据加上选项卡号。另一点需要注意的是,新增加的选项卡具有在实际增加 SlickGrid 之前修改的 CSS 属性。如果不这样做,那么网格将无法正确呈现。

方法 openTab 调用方法 add_grid,它完成 SlickGrid 对象的一个真实实例。清单 12 显示出应用程序任务繁重。可创建两个 JavaScript 对象 — workbookgrid_referencesWorkbook 对象包含到当前 workbook 对象的引用,而 grid_references 包含到每个 SlickGrid 对象的引用。add_grid 方法接受两个参数:网格名与网格号。

在列定义中,需要将 16 列作为默认值 — a 到 p — 采用 TextCellEditor 来在一个 SlickGrid 示例中提供。完成列定义后,要向 SlickGrid 提供参数定义。单元格可编辑、可调整、可选择。要注意确保 asyncEditorLoading 选项已设置为 True ,这用于在网格上实现 Ajax 操作。

清单 12. 为应用程序增加 SlickGrids
var workbook = {};
var grid_references = {};

function add_grid(grid_name, gridNumber){
    var grid;
    var current_cell = null;

    // column definitions
    var columns = [
         {id:"row", name:"#", field:"num", cssClass:"cell-selection", width:40, 
         cannotTriggerInsert:true, resizable:false, unselectable:true },
         {id:"a", name:"a", field:"a", width:70, cssClass:"cell-title", 
              editor:TextCellEditor},
         {id:"b", name:"b", field:"b", width:70, cssClass:"cell-title", 
              editor:TextCellEditor},
         {id:"c", name:"c", field:"c", width:70, cssClass:"cell-title", 
              editor:TextCellEditor},
         {id:"d", name:"d", field:"d", width:70, cssClass:"cell-title", 
              editor:TextCellEditor},
         {id:"e", name:"e", field:"e", width:70, cssClass:"cell-title", 
              editor:TextCellEditor},
         {id:"f", name:"f", field:"f", width:70, cssClass:"cell-title", 
              editor:TextCellEditor},
         {id:"g", name:"g", field:"g", width:70, cssClass:"cell-title", 
              editor:TextCellEditor},
         {id:"h", name:"h", field:"h", width:70, cssClass:"cell-title", 
              editor:TextCellEditor},
         {id:"i", name:"i", field:"i", width:70, cssClass:"cell-title", 
              editor:TextCellEditor},
         {id:"j", name:"j", field:"j", width:70, cssClass:"cell-title", 
              editor:TextCellEditor},
         {id:"k", name:"k", field:"k", width:70, cssClass:"cell-title", 
              editor:TextCellEditor},
         {id:"l", name:"l", field:"l", width:70, cssClass:"cell-title", 
              editor:TextCellEditor},
         {id:"m", name:"m", field:"m", width:70, cssClass:"cell-title", 
              editor:TextCellEditor},
         {id:"n", name:"n", field:"n", width:70, cssClass:"cell-title", 
              editor:TextCellEditor},
         {id:"o", name:"o", field:"o", width:70, cssClass:"cell-title", 
              editor:TextCellEditor},
         {id:"p", name:"p", field:"p", width:70, cssClass:"cell-title", 
              editor:TextCellEditor},
    ];

    var options = {
          editable: true,
          autoEdit: true,
          enableAddRow: true,
          enableCellNavigation: true,
          enableCellRangeSelection : true,
          asyncEditorLoading: true,
          multiSelect: true,
          leaveSpaceForNewRows : true,
          rerenderOnResize : true
    };

    eval("var data" + gridNumber + " = [];");
    workbook["data" + gridNumber] = [];
    for( var i=0; i < 100 ; i++ ){
        var d = (workbook["data"+gridNumber][i] = {});
        d["num"] = i;
        d["value"] = "";
    }

    grid = new Slick.Grid($("#"+grid_name),workbook["data"+gridNumber], columns, options);
...

以 “类似黑客” 的方式使用 eval 语句,动态创建变量名。变量 data 将以空字符串数据方式加载,并完成 SlickGrid 实例的创建。

此时,针对事件将 attach 增加到网格,如清单 13 所示。当 onCurrentCellChanged 事件发生时,它获取网格中的数据并更新单元格内容。在完成单元格编辑之前,调用 onBeforeCellEditorDestroy 事件。然后事件触发,将会获取单元格数据,但这一次要确定第一个字符是等号。如果是,则采用 eval 方法来计算输入的表达式。

注意: 不要在生产环境中使用此代码。它会使您的系统完全开放给各类注入攻击。始终要注意净化您的数据。

最后,到网格的引用将保存以供其他方法使用。

清单 13. 向网格增加事件处理
// file: resources/js/spreadsheet.js continued
    // Events
    grid.onCurrentCellChanged = function(){
        d = grid.getData();
        row  = grid.getCurrentCell().row;
        cell = grid.getCurrentCell().cell;
        this_cell_data = d[row][grid.getColumns()[cell].field];
    };

    grid.onBeforeCellEditorDestroy = function(){
        d = grid.getData();
        row  = grid.getCurrentCell().row;
        cell = grid.getCurrentCell().cell;
        this_cell_data = d[row][grid.getColumns()[cell].field];

        if(this_cell_data && this_cell_data[0] === "="){
            // evaluate JavaScript expression, don't use
            // in production!!!!
            eval("var result = " + this_cell_data.substring(1));
            d[row][grid.getColumns()[cell].field] = result;
        }
    };
    grid_references[grid_name] = grid;
};

根据前面的规范,您的电子表格必须创建新的工作簿,以及保存并打开现有工作簿。现在实现新工作簿函数,这是三个任务中最简单的一个。在此处,所要做的就是毁掉 UI 以及任何引用,然后重新创建,如清单 14 所示。

清单 14. 创建新工作簿
$('#new').live('click', function(){
    // delete any existing references
    workbook = {};
    grid_references = {};

    // remove grid, existing forms, and recreate
    $('body').html('');

    // recreate
    render_ui();
    openTab();
});

“save” 函数有点复杂。可采用 jQuery 选择器来获取名称属性以 data 开头的每个元素,然后采用 each 方法来循环处理结果集。需要关注的重点是采用用于 “线上” 发送的插件 jquery.json 来将网格数据编码成 JSON 格式。采用 $.post 方法来异步发送数据。传递给索引视图的参数是需要执行的动作(在本例中为 save,如清单 15 所示)、唯一的表 ID、工作簿名、以及 JSON 格式的网格数据。

清单 15. 保存网格数据
$('#save').live('click',function(){
    // Do a foreach on all the grids. The ^= operator gets all
    // the inputs with a name attribute that begins with data
    $("[name^='data']").each(function(index, value){
        var data_index = "data"+index;
        var sheet_id = $('#tabs_'+index+'_form').find('#sheet_id').val();
        if(sheet_id !== ''){
          sheet_id = eval(sheet_id);
        }

        // convenience variable for readability
        var data2post  = $.JSON.encode(workbook[data_index]);
        $("#"+data_index).val(data2post);

        $.post( '{% url index %}', {'app_action':'save', 'sheet_id': sheet_id,
                'workbook_name':$('#workbook_name').val(),
                'sheet':data_index, 'json_data':data2post});
    });
});

如果重新调用,那么打开操作需要 load_sheets 方法。现在就添加该方法(见清单 16)。此方法必须调用后端,请求表,并将工作簿的所有数据传递给方法。然后将数据加载到相应的 SlickGrid 对象中。注意,在将数据插入到对象之前,必须利用 decode 方法来反序列化 JSON 数据。然后重新呈现网格。

清单 16. 将数据加载到网格中
function load_sheets(workbook_name){
    $('#workbook_list').load('{% url index %}', 
        {'app_action':'get_sheets','workbook_name':workbook_name}, 
        function(sheets, resp, t){
        sheets = $.JSON.decode(sheets);

        workbook = {}; // reset
        grid_references = {};
        $.each(sheets, function(index, value){

            // add to workbook object
            var sheet_id = value["sheet_id"];
            openTab(sheet_id);

            // By calling eval, we translate value from
            // a string to a JavaScript object
            workbook[index] = eval(value["data"]);

            // insert data into hidden
            $("#data"+index).attr('value', workbook[index]);
            grid_references["tabs_"+index].setData(workbook[index]);
            grid_references["tabs_"+index].render();

        });
    });
}

最后,需要实现 open 函数。再次进行异步调用,此次发送 list 动作,然后再次反序列化 JSON 发送的数据。接下来,利用数据库中所有工作簿的名称来更新 select 清单。对话框表单将打开。清单 17 展示了相关代码。

清单 17. 打开现有工作簿
$('#open').live('click',function(){
    // load is used for doing asynchronous loading of data
    $('#workbook_list').load('{% url index %}', {'app_action':'list'}, 
        function(workbooks,success){
        workbooks = $.JSON.decode(workbooks);
        $.each(workbooks, function(index, value){
            $('#workbook_list').append(
              '<option value="'+ value +'">'+value +'*lt;/option>');
        });
    });

    $('#dialog_form').dialog('open');
});

重新访问索引视图

此时已完成客户端 post 编码,可以看到完整的索引视图了。清单 18 展示了除导入之外的完整索引视图。

想要序列化(反序列化)JSON 代码,可采用 simplejson 模块与命令转储来对其进行序列化并为反转进行加载。对于 Save 动作,如果 ID 是针对某个表的,那么该表将被更新。否则将创建新表。

与此相反,获取表动作将创建具有特定工作簿的所有表的 JSON 对象。它利用命令 filter(),针对某个工作簿来检索所有表(QuerySet 对象)。这等效于 select * from spreadsheet_app_workbooks,其中 workbook_name = wb_name"。检索完这些设置后,再次将其转换为 JSON 格式并发回客户端。

List 也采用 Django ORM,但此次与 values() 方法一起使用。在这里,您正在指示 Django “获取 workbook_name 列”。通过在 QuerySet 对象上调用 distinct 方法,等于在说明,不想要任何重复。可再次采用清单解析,从结果创建清单。对于 get_sheetslist,必须返回 HttpResponse 对象,以便 jQuery 处理 Ajax 响应。

清单 18. 最终视图
def index(request):
    template = 'index.html'

    app_action = request.POST.get('app_action')
    posted_data = request.POST.get('json_data')

    if posted_data is not None and app_action == 'save':
        this_sheet = request.POST.get("sheet")
        this_workbook = request.POST.get("workbook_name")
        sheet_id = request.POST.get("sheet_id")

        posted_data = json.dumps(posted_data)

        if(sheet_id):
            wb = Workbooks(id=sheet_id, workbook_name=this_workbook, 
                   sheet_name=this_sheet, data=posted_data)
        else:
            wb = Workbooks(workbook_name=this_workbook, 
                   sheet_name=this_sheet, data=posted_data)
        wb.save()

    elif app_action == 'get_sheets':
        wb_name = request.POST.get('workbook_name')
        sheets = Workbooks.objects.filter(workbook_name=wb_name)

        # use list comprehension to create python list which is like a JSON object
        sheets = [{ "sheet_id":i.id, "workbook_name": i.workbook_name.encode("utf-8"),
                    "sheet_name": i.sheet_name.encode("utf-8"), 
                    "data": json.loads(i.data.encode("utf-8"))} for i in sheets ]

        # dumps -> serialize to JSON
        sheets = json.dumps(sheets)

        return HttpResponse( sheets, mimetype='application/javascript' )

    elif app_action == 'list':
        workbooks = Workbooks.objects.values('workbook_name').distinct()

        # use list comprehension to make a list of just the work books names
        workbooks = [ i['workbook_name'] for i in workbooks ]

        # encode into json format before sending to page
        workbooks = json.dumps(workbooks)

        # We need to return an HttpResponse object in order to complete
        # the ajax call
        return HttpResponse( workbooks, mimetype='application/javascript' )

    return render_to_response(template, {},
           context_instance = RequestContext( request ))

完成网格

此时已经定义了所有的 JavaScript 方法,可以利用两个方法调用来生成应用程序了,如清单 19 所示。

清单 19. 页面加载时需要执行的代码
$(document).ready(function(){
    render_ui();
    openTab();
});

完成页面加载后,UI 呈现出来,新的选项卡插入到了工作簿中。

现在可以通过在命令行运行 python manage syncdb 创建数据库,来展示电子表格应用程序的功能了。当操作完成后,执行命令 python manage.py runserver 并导航到 http://localhost:8000/spreadsheet_app。应当能看到最终版本,如图 3 所示。

图 3. 完整电子表格应用程序
截图包含 7 列,14 行,并填有客户端信息。
截图包含 7 列,14 行,并填有客户端信息。

可向项目添加很多内容,比如:

  • 图表
  • 针对单元格中输入的公式的、更安全、功能更好的解析器
  • 导出为其他格式,比如 Microsoft® Office Excel
  • 采用附加 jQuery 插件的真实菜单

结束语

虽然这一 web 应用程序还不能用于生产环境,但它演示了如何将多项技术组合在一起。采用 JavaScript、语义 HTML、JSON 对象来实现服务器数异步传输据,最重要的是,有很多现成的 jQuery 插件可供使用,这就大大简化了您的工作。


相关主题


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Web development
ArticleID=807790
ArticleTitle=利用 Django 与 jQuery 来创建电子表格应用程序
publish-date=03312012