目次


Django と jQuery を使ってスプレッドシート・アプリケーションを作成する

Comments

この記事では、jQuery、jQuery のプラグイン、そして Django を使用して単純な Web ベースのスプレッドシートを実装する方法を説明します。このスプレッドシートは決して完全なものではなく、また Google Docs と張り合おうとしているわけでもありません。あくまでも、多数の jQuery プラグインおよびツールが使用できるようになっている今、Office スタイルの Web アプリケーションをいとも簡単に作成できることを明らかにするためのデモを目的として実装するものです。私はバックエンドとして 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 を使用することにした理由は、例えばセルのグループの計算処理と解析機能に手を加える必要がある場合、SlickGrid でなら、セルのグループを強調表示/選択することができるからです。また、スクロールしながらデータをロードすることもできます。優れた jQuery グリッドには、他にも Flexigrid、jQuery Grid、jqGridView、Ingrid などがあります。さらに、jQuery プロジェクトでは公式 jQuery Grid プラグインの計画も発表しています。

スプレッドシートの仕様

各スプレッドシートは 1 つのワークブックで構成され、各ワークブックは 1 つ以上のデータ・シートで構成されます。シートの各セルは、入力された先頭文字が等号 (=) であれば、演算処理を実行します。そうでなければ、入力されたテキストは、入力されたままの状態になります。データは JSON オブジェクトにロードされ、非同期でバックエンドに送信されてデータベースに保存されます。スプレッドシートが対応する操作は、open (開く)、new (新規作成)、save (保存) です。ワークブックの名前は最上部の編集可能テキスト・ボックスに表示されます。

open (開く)」をクリックすると、jQuery UI ウィンドウが開き、データベースに保存されている既存のワークブックが表示されます。そのなかからワークブックを選択すると、保存されている JSON データが Ajax (Asynchronous JavaScript and XML) によって取得され、グリッドにレンダリングされます。ワークブックを保存すると、グリッドのデータは JSON フォーマットで非同期にバックエンドに送信されて保存されます。新規作成操作では参照をすべて消去して、クリーンなワークブックを新たにロードします。

最後に、ワークブックのシートは個々の jQuery UI タブに分割されます。タブは、他のスプレッドシートと同じく最下部に表示されます。最下部のボタンをクリックすると、新しいタブが動的に追加されます。

プロジェクトの構造

すべての 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) をご自由に使用してください。リスト 1 のコードを使用するように、settings.py を変更します。

リスト 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 つだけです。1 つは静的に提供されるファイルのマッピング、もう 1 つはインデックス・ビューを指すマッピングです。リスト 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 を参照)。このモデルには、workbook_namesheet_namedataという 3 つのフィールドしかありません。id というキー・フィールドは、Django ORM (Object-Relational Mapper) によって自働的に作成されます。

リスト 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 の能力を最大限には活用していません。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':
        ...

上記のリストを見るとわかるように、インポートが完了した後、インデックス・ビューは、クライアントから送信されたポスト・データが含まれるリクエスト・オブジェクトを受け入れます。このオブジェクトで受け取るパラメーターは、app_actionposted_data の 2 つです。app_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 には制御ロジックも、スタイル設定もありません。ここにあるのはマークアップだけです。実のところ、body には要素さえも含まれていません。要素はすべて 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 を参照)。メニューは 3 つのボタン (new、open、save) と、ワークブック名を表示および入力するためのテキスト・フィールドで構成されます。jQuery の prepend を使用しているのは、マークアップが追加されるときに、body の最初の要素として挿入されることを確実にするためです。

リスト 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」という 3 つのタブと、テキスト・ボックスに「invoices_2010」という名前が表示されているスプレッドシート
「new」、「open」、「save」という 3 つのタブと、テキスト・ボックスに「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 セレクターを使用して tabs<div> への参照を取得した後、tabs() メソッドを呼び出して <div> をタブ・ウィジェットに変換します。デフォルトの CSS クラス ui-corner-top は削除されて、ui-corner-bottom クラスが追加されるので、タブはワークブックの最下部に表示されます (図 2 を参照)。

図 2. ワークブックのタブ
「Sheet 0」、「Sheet 1」、「Sheet 2」という名前が表示された 3 つのタブ

このコンポーネントが、すべてのデータ・グリッドのコンテナーとなります。今度はタブ・コンポーネントの下にボタンを追加し、このボタンがクリックされるたびに、動的にタブが追加されるようにします。それには、以下の add_newtab_button メソッドを使用します。

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

最後に作成する可視コンポーネントは、「Open (開く)」ウィンドウです。このウィンドウは、insert_open_dialog_markup メソッドを呼び出して作成します (リスト 9 を参照)。他の挿入マークアップ・メソッドと同様に、このメソッドもマークアップ情報が含まれるストリングを作成して、そのストリングを body に追加します。

リスト 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);
}

ウィンドウのマークアップが準備できたところで、このウィンドウに機能を追加します。そのために使用するのは、render_ui の中で最後に呼び出されている make_open_dialog メソッドです (リスト 10 を参照)。.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 (開く)」ウィンドウはモーダル・ウィンドウなので、ページのロード時には開きませんこのウィンドウには 2 つのボタン、「OK」と「Cancel (キャンセル)」も表示します。後者のボタンはただウィンドウを閉じるだけですが、OK 関数は選択リストから選択された項目を見つけて、その値を取得してからウィンドウを閉じます。そして body 内からすべての子要素を削除し、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 が従う命名規則は、データの後にタブ番号が続くという単純なものです。もう 1 つ注意しなければならない重要な点として、新しく追加されるタブの CSS 属性は、実際の SlickGrid を追加する前に変更されます。こうしないと、グリッドは誤ってレンダリングされることになってしまいます。

openTab メソッドは、SlickGrid オブジェクトを実際にインスタンス化する add_grid メソッドを呼び出します。リスト 12 に示すのは、このアプリケーションの力仕事の部分です。このリストは、workbookgrid_references という 2 つの JavaScript オブジェクトを作成します。workbook オブジェクトには現在対象としているワークブック・オブジェクトへの参照が格納される一方、grid_references には各 SlickGrid オブジェクトへの参照が格納されます。add_grid メソッドが取る引数は 2 つあり、グリッド名とグリッド番号です。

列定義では、SlickGrid サンプルのうちの 1 つに提供されている TextCellEditor を使用して、a から p の 16 の列をデフォルトとして設定します。列定義の後に続くのは、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 インスタンスを作成しています。

今度は、グリッドとイベントとの関連付けを追加します (リスト 13 を参照)。onCurrentCellChanged イベントが発生すると、グリッド・データが取得されて、セルの内容が更新されます。onBeforeCellEditorDestroy イベントは、セルの編集が完了する前に呼び出されます。このイベントがトリガーされたら、以前と同じセルのデータを取得しますが、この場合には最初の文字が等号であるかどうかを判別します。等号となっている場合は、JavaScript の 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;
};

もう一度仕様に立ち返ると、このスプレッドシートは新規ワークブックを作成できるだけなく、既存のワークブックを保存することも、開くこともできなければなりません。この 3 つのタスクのうち、まずは最も単純な、新規ワークブックを作成する new 関数を実装します。この場合、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 メソッドを使用して非同期で行います。インデックス・ビューに渡すパラメーターは、実行するアクション (この例では、リスト 15 に示されているように save アクションです)、一意のシート 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 を参照)。load_sheets メソッドはバックエンドを呼び出して、メソッドに渡されたワークブックのシートとそのすべてのデータを要求します。そして、データを対応する 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();

        });
    });
}

最後になりましたが他の 2 つの関数と同じく重要な、ワークブックを開く 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');
});

インデックス・ビューの詳細

クライアント・サイドのポスト送信のコーディングは完了したので、ここからはインデックス・ビューを完成させる作業に取り掛かれます。インポートを除くすべてのインデックス・ビューについては、リスト 18 を参照してください。

JSON コードへのシリアライズを行うには、simplejson モジュールの dumps コマンドを使用し、JSON コードからのデシリアライズをするには loads コマンドを使用します。save アクションでは、個々のシートの ID が指定されている場合はシートを更新し、指定されていなければ新しいシートを作成します。

これとは対照的に、get_sheets アクションでは、ある特定のワークブックの全シートの情報が含まれる JSON オブジェクトを作成します。ワークブックのシート (QuerySet オブジェクト) をすべて取得するためには、「select * from spreadsheet_app_workbooks where workbook_name = wb_name」に相当する filter() コマンドを使用します。シートのセットを取得した後、これらのシートを再び JSON フォーマットに変換してクライアントに送信します。

list アクションも同じく Django ORM を使用しますが、ここで使用するメソッドは values() です。つまり、Django に「workbook_name 列を取得」するように指示することになります。QuerySet オブジェクトの distinct メソッドを呼び出すことで、重複したワークブックは不要であることを明示的に指示します。そして再度、リスト内包表記を使用して結果からリストを作成します。get_sheetslist はどちらも、jQuery が Ajax レスポンスを処理できるように、HttpResponse オブジェクトを返す必要があります。

リスト 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 に記載する 2 つのメソッド呼び出しを使ってアプリケーションを生成することができます。

リスト 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=60
Zone=Web development
ArticleID=645725
ArticleTitle=Django と jQuery を使ってスプレッドシート・アプリケーションを作成する
publish-date=03152011