[IM]INTER-Mediatorワークショップで作った“イベント参加申し込みサイト”

K-OFのイベントの1つとして、INTER-Mediatorのワークショップを2015年11月7日に開きました。ワークショップとなると、参加者の皆さんに手をうごしてもらって…というのが一般的ですが、その場で参加される方々もいらっしゃったので、手を動かすのはプレゼンターだけでやりました。

今回のワークショップでの開発目標は、イベントの参加申し込みです。別の勉強会での事後アンケートで、「こういうのを作りたい」と希望があり、それを題材にさせていただきました。グループカウンセリングを実施している赤石さんは、現在、フォームズというサービスで参加受付を行い、メールで送られてくる参加依頼の内容を1項目ずつコピー&ペースとしているそうです。当初は「コピペをしないで一発でExcelに入れる」という要望だったのですが、いっそのこと、受付フォームと一覧表示をINTER-Mediatorで作成するのはどうかということで了承していただき、それをワークショップの題材としました。なお、実際の運用サイトでは、以下のコード内にあるパスワードや一部の名称はもちろん変更しています。

12191812_909228155821822_1194109405261025992_n

設計内容とスキーマ

ざっくり書いたユーズケース図は次の通りです。アクターは、赤石さんと、グループカウンセリングに参加される「お客様」の2つです。お客様はブラウザで申し込みをして、その結果をメールで受け取ることになります。その申し込み結果を「申し込みリスト」に蓄積して、それを赤石さんは随時チェックするという流れになります。この時、お客様は認証等なく申し込みを行えるようにしますが、一方で赤石さんしかリストが見れないようにします。ここで、データベースへの新規レコードは認証なし、その他の処理は認証を行わないとできないようにするという基本設計もできてしまいます。また、システムとアクターの接点は2か所あり、それぞれ1ページだけで実現しそうなので、合計2つのWebページの作成で済みそうです。

ユースケース図0

ER図と同じ用途のクラス図は次のようになります。ちょっとシンプルですが、「申し込み」という1つのテーブルで作ります。本来、グループカウンセリングの実施が1つのエンティティになり、カウンセリングのテーブルを作成して、申し込みとイベントが多対1の関係を作ります。現状では1種類のイベントを、シーケンシャルに、つまり同時に複数のイベントの募集をしているといった事情もないので、1テーブルで運用します。言い換えれば、イベントには「開催日時」というフィールドしかないので、別テーブルにする必要性は薄いということです。「義援金支払い許諾」はチェックボックスで、受講条件の1つを許諾するかどうかを明確にするものです。「ユーザー情報」は、INTER-Mediatorで認証を実施するために必要なテーブル群です。

クラス図0

これらを実現するMySQL用スキーマとして、以下の記述を作成しました。実はワークショップでは、これに失敗して悩んでしまったのですが、今後のワークショップはこれを雛形にしましょう(苦笑。ユーザーやデータベースを定義してアクセス権を設定し、applyテーブル(クラス図での「申し込み」)を定義します。その後の、authuser、authgroup、authcor、issuedhashのテーブルは認証で必要とするものです。リスト表示の時に使用するuser1ユーザーのみ定義しています。

SET NAMES 'utf8mb4';
#DROP USER 'web'@'localhost';
CREATE USER 'web'@'localhost' IDENTIFIED BY 'password';
DROP DATABASE IF EXISTS akaishi;
CREATE DATABASE akaishi CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE akaishi;
GRANT SELECT, INSERT, DELETE, UPDATE ON TABLE akaishi.* TO 'web'@'localhost';

CREATE TABLE apply (
  apply_id INT AUTO_INCREMENT,
  name VARCHAR(100),
  yomi VARCHAR(100),
  email VARCHAR(100),
  address VARCHAR(255),
  content TEXT,
  agreement INT,
  startdt DATETIME,
  PRIMARY KEY(apply_id)
) CHARACTER SET=utf8mb4, COLLATE=utf8mb4_unicode_ci, ENGINE=InnoDB;

CREATE TABLE authuser (
  id INT AUTO_INCREMENT,
  username VARCHAR(48),
  hashedpasswd VARCHAR(48),
  email VARCHAR(100),
  limitdt DATETIME,
  PRIMARY KEY(id)
) CHARACTER SET=utf8mb4, COLLATE=utf8mb4_unicode_ci, ENGINE=InnoDB;

INSERT authuser SET id=1,username='user1',hashedpasswd='d83eefa0a9bd7190c94e7911688503737a99db0154455354';

CREATE TABLE authgroup (
  id INT AUTO_INCREMENT,
  groupname VARCHAR(48),
  PRIMARY KEY(id)
) CHARACTER SET=utf8mb4, COLLATE=utf8mb4_unicode_ci, ENGINE=InnoDB;

CREATE TABLE authcor (
  id INT AUTO_INCREMENT,
  user_id INT,
  group_id INT,
  dest_group_id INT,
  privname VARCHAR(48),
  PRIMARY KEY(id)
) CHARACTER SET=utf8mb4, COLLATE=utf8mb4_unicode_ci, ENGINE=InnoDB;

CREATE TABLE issuedhash (
  id INT AUTO_INCREMENT,
  user_id INT,
  clienthost VARCHAR(48),
  hash VARCHAR(48),
  expired DateTime,
  PRIMARY KEY(id)
) CHARACTER SET=utf8mb4, COLLATE=utf8mb4_unicode_ci, ENGINE=InnoDB;

申し込みページの作成

まず、お客様がイベントの申し込みを行うページを作ります。コンテキストの定義を含む定義ファイル(apply.php)は以下のようなものです。ポストオンリーモードでのページを作成すればいいので、コンテキストは1つだけです。また、新規レコードは認証なしで、その他のデータベース処理はできないようにしたいので、存在しないユーザー(nobodyknows)に対してのみ読み出し、更新、削除ができるようにして、事実上、これらの3つの処理は行えないようにします。authenticationキーにcreateキーの値がないので、createに関しては認証なく実施できます。オプション指定はなく、データベース指定のところに、dsn、user、passwordのキーで、データベースへの接続についての記述を行っています。

<?php
require_once('../INTER-Mediator/INTER-Mediator.php');

IM_Entry(
  array(
    array(
      'name' => 'apply',
      'table' => 'apply',
      'view' => 'dummy',
      'key' => 'apply_id',
      'post-reconstruct'=>true,
      'authentication' => array(
        'read' => array('user' => 'nobodyknows'),
        'update' => array('user' => 'nobodyknows'),
        'delete' => array('user' => 'nobodyknows'),
      ),
      'send-mail' => array(
        'create' => array(
          'from-constant' => "msyk@msyk.net",
          'cc-constant' => "msyk@msyk.net",
          'subject-constant' => "申し込みを受け付けました",
          'to' => "email",
          'body-template' => "confirm.txt",
          'body-fields' => "name,yomi,email,address,content,startdt",
        ),
      ),
    ),
  ),
  array(),
  array(
    'db-class' => 'PDO',
    'dsn' => 'mysql:host=localhost;dbname=akaishi;charset=utf8mb4',
    'user' => 'web',
    'password' => 'password',
  ),
  false
);

お客様が申し込みを行うページのぺージファイル(apply.html)は次のように定義しました。見出しの文字列は最低限にしていますが、実運用の時には書き直して利用していただくとして、INTER-Mediatorの動作に関わる部分だけを作ってあります。ヘッダ部での定義ファイルの読み込み、BODYタグのonload属性でのフレームワーク呼び出し、そしてボディ部での記述がポイントになります。ボディ部では、TABLEタグによるテーブルが定義されていますが、data-im-control属性により、ポストオンリーモードであることが指定されています。あとは、コンテキストとフィールド名のセットをdata-imに記述します。同意の項目はチェックボックスですが、value=1の指定があり、チェックがあれば1をフィールドに設定します。カウンセリングの日付はstartdtフィールドにしますが、ここの部分はお客様が入力するのではなく、募集するときに日時が決定しているので、その日時を初期値として規定し、INPUTタグ自体はreadonly属性を設定して、表示だけをするようにします。最後のBUTTONもdata-im-control属性を指定して、書き込みボタンにします。なお、定義ファイルのコンテキストにはpost-reconstructキーのみがあり、この場合は書き込みを行うとこのページの再ロードを行うので、そこで入力した結果が消えます。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
  <script type="text/javascript" src="apply.php"></script>
</head>
<body onload="INTERMediator.construct();">
  <table>
    <tbody data-im-control="post">
      <tr>
        <th>名前</th>
        <td><input type="text" data-im="apply@name"/></td>
      </tr>
      <tr>
        <th>名前の読み</th>
        <td><input type="text" data-im="apply@yomi"/></td>
      </tr>
      <tr>
        <th>メールアドレス</th>
        <td><input type="text" data-im="apply@email"/></td>
      </tr>
      <tr>
        <th>住所</th>
        <td><input type="text" data-im="apply@address"/></td>
      </tr>
      <tr>
        <th>相談内容</th>
        <td><textarea data-im="apply@content"></textarea></td>
      </tr>
      <tr>
        <th>同意</th>
        <td><input type="checkbox" value="1" data-im="apply@agreement"/></td>
      </tr>
      <tr>
        <th>日程</th>
        <td><input type="text" data-im="apply@startdt" 
                   value="2015-11-07 13:00:00" readonly/></td>
      </tr>
      <tr>
        <th></th>
        <td><button data-im-control="post">申し込む</button> </td>
      </tr>
    </tbody>
  </table>
</body>
</html>

定義ファイルapply.phpのコンテキストには、レコードを作成した時にメールを送信する設定が組み込まれています。そのためのメールの文面は、confirm.txtファイルに以下のように用意します。メールについては、定義ファイルでの定義通り、差出人、CC、件名は決められたものを設定し、送り先は作成されたレコードのemailフィールドを指定します。本文は、以下のconfirm.txtの中の@@1@@などの部分が新規に作成されたレコードの値に置き換わります。body-fieldsキーの値に列挙されたフィールドとその順番より、@@1@@が名前、@@3@@がメールアドレス、@@5@@が相談内容、@@6@@が開催日時を示します。

@@1@@ 様(メールアドレス:@@3@@)

以下の通り、グループカウンセリングのお申し込みを受け付けました。

開催日時:@@6@@
ご相談内容:
@@5@@

_(などなど必要な情報を追加する)_

作成したページは次のようなものです。

shot3036

申し込みを行うと、入力したメールアドレスに、確認のメールが送られます。

shot3041

一覧ページの作成

一覧ページを構成するために、以下のような定義ファイル(list.php)を作成しました。こちらは、applyテーブルの内容を参照したり、場合によっては変更や削除等があるので、すべてのデータベース操作に対して、user1で認証したユーザーだけが許可されるようにしました。また、開催日の逆順で一覧されることで、最近の開催日に対する参加者がリストの上部にまとまるようにしました。コンテキストには、削除ボタンの追加の指示はありますが、挿入ボタンはありません。挿入ボタンは、必要ならapply.htmlから自分で申し込みを入れてそれを修正すればいいと考えました。

<?php
require_once('../INTER-Mediator/INTER-Mediator.php');

IM_Entry(
  array(
    array(
      'name' => 'apply',
      'table' => 'apply',
      'view' => 'apply',
      'key' => 'apply_id',
      'paging' => true,
      'records' => '20',
      'repeat-control' => 'confirm-delete',
      'sort' => array(
        array('field' => 'startdt', 'direction' => 'desc'),
      ),
      'authentication' => array(
        'all' => array('user' => 'user1'),
      ),
    ),
  ),
  array(),
  array(
    'db-class' => 'PDO',
    'dsn' => 'mysql:host=localhost;dbname=akaishi;charset=utf8mb4',
    'user' => 'web',
    'password' => 'password',
  ),
  false
);

一覧のページファイル(list.html)は以下の通りです。特に変わったところはなく、applyコンテキストの内容をページネーションとともに表示をしています。コンテキスト定義にあるように、20レコードずつ表示をします。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <link rel="stylesheet" href="INTER-Mediator/Samples/sample.css" />
  <title>Title</title>
  <script type="text/javascript" src="list.php"></script>
  <style>
    td{
      border: solid 1px black;
    }
  </style>
</head>
<body onload="INTERMediator.construct();">
  <div id="IM_NAVIGATOR"></div>
  <table>
    <thead>
      <tr><th>開始日時</th><th>名前</th><th>読み</th>
        <th>住所</th><th>相談内容</th><th>同意</th><th></th></tr>
    </thead>
    <tbody>
      <tr>
        <td data-im="apply@startdt"></td>
        <td data-im="apply@name"></td>
        <td data-im="apply@yomi"></td>
        <td data-im="apply@address"></td>
        <td data-im="apply@content"></td>
        <td data-im="apply@agreement"></td>
        <td></td>
      </tr>
    </tbody>
  </table>
</body>
</html>

ページを表示すると、ログインパネルが表示されます。

shot3042

正しいユーザー名とパスワードを入力してログインすれば、リスト形式で、申込者の一覧を見ることができます。

shot3043

ワークショップを終えて

結果的にワークショップでは、入力専用(ポストオンリーモード)のページと、その入力した結果を表示するための2つのページを作成しました。実際のワークショップでは凡ミスに気づかずちょっと時間を無駄しにしてしまって申し訳なかったのですが、通常ではこれくらいであれば1時間程度の作業で行えるということが示せたかと思います。また、典型的なシステム開発の流れも見ていただけるサンプルになったかと思います。

なお、こうして作ったシステムも、作ってから新たなニーズが発生するかと思います。そうした追加の要求や変化する要求についても追跡させてもらって、可能な限り、アフター・ワークショップとしてお伝えできればと考えています。

買ったばかりのMacBookのUSB-C事情

MacBookは5/17に到着、5/20に「USB-C Digital AV Multiportアダプタ」が到着、注文中の「USB-C VGA Multiportアダプタ」はまだ未着の段階です。DELLの4Kディスプレイで、Retina動作をすることを報告しました。これはある種の「予想外の動作」と言えるでしょう。しかしながら、使うに従って、USB-Cについて、いろいろ試行錯誤が必要になってきました。

まず、Nexus 7のACアダプタ(7Wか10Wだと思う)で、充電できることをFacebookにも書きましたが、場所によって充電できる場合とできない場合があることが判明しました。自宅だと、充電できます。しかし、移動先ではできたりできなかったりします。同一のアダプタ、同一のケーブル(USB-AとUSB-Cのケーブルを購入した)でやっているので、これはおそらくコンセントの段階での電圧の問題ではないかと思います。先日、「充電できる」と思っていたら、「充電できない」という状況に遭遇しました。とはいえ、1日程度はなんとかバッテリーは持ちました。

次にディスプレイアダプタ(USB-C Digital AV Multiportアダプタ)を装着すると、Nexus 7のACアダプタでは、100%充電はできないし、給電もできないことがわかりました。アダプタが電源を食うのか、USBの機材の関係なのか、ともかくNexusのアダプタ経由では充電や給電ができないのです。

しかし、なんと、iPadに付属のACアダプタ(12W)だと、ディスプレイアダプタを経由して、MacBookに給電/充電ができるのです。ということは、持ち歩きはiPadのアダプタでいいということになりますね。iPadは、Nexusのアダプタで使えばいいということで、「予想外の嬉しい組み合わせ」がもう1つ見つかりました。

代替のACアダプタ、つまり、軽いとか、安いのでいくつか買ってあっちこちにキープみたいなことができないかと考えるわけですが、USB出力があって、10Wを超える(つまり、1つのポートで20W)みたいな製品ってAmazonで調べたけど、見当たりませんでした。USB出力のACアダプタは、ほぼ、スマホタブレット向けで、出力が低いのです。20Wと書いてあっても2端子合計だったりするのが普通のようです。iPadのACアダプタは2200円なので、純正品が5800円に対して半額以下であり、大きさも小さいということもあり、ある意味、持ち歩き用としては、買い足してもいい範囲なんじゃないかと思います。

USB-Cのケーブルもまだまだ高いですし、USB-Cの反対側が高出力のACアダプタ向けのものなんてまだ見当たりません。ということで、新しいMacBookユーザが欲しいスペックのブツはどうやらまだまだのようです。要するに純正以外の選択肢が非常にない状態ということです。予測はしていましたが、とにかく乗り切らねば。Facebookでリンクを紹介しましたが、クラウドファンディングで募集しているHub+がまずは新しいパターンの製品ですね。mini Display 端子があるので、Apple純正のディスプレイをつなぐことができます。

ディスプレイアダプタが来て本格的に利用

新しいMacBookは日曜日に来ました。そして、今日水曜日に遅れてUSB-C Digital Multiport Adapterが到着し、やっと大画面ディスプレイと接続ができました。薄っぺらいもののちょっと大きなアダプタですが、3つのコネクタを横に並べるとしたら、やっぱりこのサイズになるでしょうか。USB-Cには本体付属のケーブルで、付属のACアダプタへ接続しています。ディスプレイとは、ディスプレイに付属のUSBケーブルと、購入したHDMIケーブルで接続をしています。

IMG_0383

このMacについてを見てみると、ディスプレイフルの解像度で見えています。4Kということで、4000弱の横ドット数です。

shot2012

ところが、「システム情報」を見てみると、1920×1080となっており、ディスプレイ自体のちょうど半分の解像度です。これは、Appleの27インチのCinema Displayの解像度(2560×1440)より低い解像度となります。Command+Shift+3で画面ショットを作成すると、3840×2160ドットなので、ディスプレイのフルの解像度を利用していることがわかります。

shot2013

システム環境設定の「ディスプレイ」では、外付けディスプレイも、内蔵ディスプレイと同じように、ドット数ではなく、4段階の選択になっています。つまり、外付けのRetinaディスプレイとして扱っていると考えていいのではないでしょうか。

shot2011

実は直前まで使っていたのはMacBook Airで、RetinaディスプレイのMacは今回購入したMacBookが初めてなんです。もちろん、美しいのは当然ですが、外付けディスプレイまで、Retinaの処理をしているのは知りませんでした。しかしながら、外付けディスプレイの表示結果は、内蔵ディスプレイに比べて「ちょっと大きい」感じがします。27インチのディスプレイの、従来の解像度よりも低い解像度だけに大きく見えるのは当然でしょう。内蔵ディスプレイの内容がやや小さいので、並べると外付け側が大きい感じが出ます。

しかし、最近、視力も落ち気味だけに、ちょうどいいかとも思っています。システム環境設定の「ディスプレイ」で「スペースを拡大」側のいちばん右にすると、さすがに小さすぎます。しかし、これは、もしかして、5Kだったら大きさ感が一致するのかもしれません。もしかして、それが理由で4KディスプレイをAppleは出さない(部品がなくてだせないとか?)のかもとも思ってしまいます。

Yosemiteでdocdiff

以前、2つの文書の比較結果をカラーリングで示せるdocdiffのことをMavericks向けに書きました。Yosemiteで同じようにすると動きませんでした。しかし、gemで簡単にインストールできるようです(松尾さんありがとう)。

sudo gem install docdiff

これで、/usr/bin/docdiffがコマンドとして機能するようになります。そのほかのライブラリファイル群は正しい場所に入っているということで、気にしないで行きます。

さて、svnで使うには、パラメータの組み換えが必要です。そこで、/usr/local/binにdocdiffを作ってそれを実行用にします。もちろん、viでもemacsでもご自由なエディタで編集してください。もし、/usr/local/binが作られていないのなら、mkdirコマンドで作ってください。

sudo nano /usr/local/bin/docdiff

中身はこのように書きます。

#/bin/sh
/usr/bin/docdiff --format=tty $6 $7

そして、もちろん、アクセス権を設定しておきます。

sudo chmod a+x /usr/local/bin/docdiff

こうすれば、以下のようにsvnコマンドを打ち込むときに、docdiffを使って比較を行います。最後のパラメータはファイル名です。

svn diff --diff-cmd=docdiff -r PREV _any_file_name_

もし、パス(echo $PATH で確認できます)が、/usr/local/binに通っていないとか、/usr/binより後にある場合には、上記のパラメータの1つを「–diff-cmd=/usr/local/bin/docdiff」のように指定します。

ということで、docdiffは便利です。

shot2001

ディスプレイをDell P2716Qに置き換えた

Apple LED Cinema Display 27インチは売り先が決まりそうなので、さっそくディスプレイを買い換えました。新しいMacBookにリプレースすると、Cinema Displayへの接続がとりあえずはできない(そのうちできるかもしれないけど)ので、HDMIのあるものに切り替える必要があります。以前のブログ記事のように、BenQの24インチもありますが、もう少しクオリティたかそうなものが欲しいのと、せっかく新しいMacBookなので、4Kにしようと思い、Dell P2715Qに変えました。

しかしながら、まだ新しいMacBookは到着していないので、本体はMacBook Air 2012年モデルのままです。Mini Display Portから直接つないでみましたが、もちろん、つないでほぼ即座に使えました(最初に入力元の切り替えだけ必要)。

IMG_0381

大きさ感はCinema Displayとさほど変わりません。もちろん、筐体のゴージャスさは大きく差がありますが、まあ、それは仕方ないでしょう。MacからはUSBケーブル1本で、ディスプレイ側に接続し、そこから4つのUSBケーブルを接続できます。USBのハブの出力先は1本増えたのは嬉しいところ。ただし、MacBook Airへの電源供給がなくなったので、ACアダプタを自宅でもしばらく使うことになります。

システム環境設定のディスプレイで見ると、本体が4K対応ではないので、2560×1440しか出ません。Cinema Displayだとそれが最大の解像度だったのでくっきり見えていましたが、Dell P2715Qだと、若干「解像度落としてます感」がありますね。まあ、でも気になりません。画質もBeQの24インチより色が自然な感じです。IPSだからというのもあるかもしれません。まあ、3倍くらい高いから当たり前といえば当たり前かもしれません。「プリセットモード」ってのがあるのですね。けっこう色調が変わりますが、とりあえずスタンダードです。

shot0319

システム環境設定の「サウンド」の出力を見ると、DELL P2715Qと見えているのでおや?と思ったのですが、単に項目として出るだけですね。サウンドがないのはもともと知っていました。下手なスピーカーをつけるより、正しい設計だと思います。そういえば、ラインアウトがあるので、つないだら何か出てくるのだと思います。

shot0318

大画面が2枚欲しいと言いながら、実のところ、クオリティ差が大きいこともあって、結局1枚しか使わない状態になりました。ただし、MacBookが来たら、Retinaだし、トラックパッドも新しくなります。ということで、多分、Dellのディスプレイの前にMacBookを置くパターンで作業するでしょうね。ということで、BenQ 24インチはまたまた、使う機会が減ったままです。実は、今ところ、Apple TVとつないで、大画面ラジオ(意味ない〜笑)にしか使っていない状態です。まあ、でも、たまに画面が欲しくなるときもあるだろうから、AirPlayでスタンバイのままで行くでしょう。

などと書きながら考えて、こういうセッティングにしてみました。右側のBenQ 24インチの後ろに、PromiseのPegasus R4とMac mini Serverがあります。なんとなく、スタンドの位置は夜になって悩む気がしますが、MacBookが来るまでの2、3週間はこの体制かなと思いっています。右の方の机は、もちろん片付けます(笑)。

IMG_0382

Yosemiteのウインドウの緑ボタンは最大化でなくフルスクリーン

Yosemiteのウインドウのタイトルバーにある左側のボタンがすっかりデザインが変わりましたが、もっと重要な違いがあります。緑のボタンをクリックすると、今までは最大化されましたが、Yosemiteではフルスクリーンモードになります。

スクリーンショット 2014-10-17 17.49.05

一方、タイトルバーをダブルクリックするか、あるいはoptionキーを押しながら緑のボタンをクリックすることで、従来の「ウインドウ最大化」と戻しができます。ただし、Chromeではタイトルバーをダブルクリックでは最大化されないので、アプリケーションの実装によりできない場合もあるかも。

緑のボタンを押してフルスクリーンにしてしまうと、メニューはでなくなります。また、タイトルバーの右端にあったフルスクリーンから戻すボタンもなくなってしまって一瞬慌てます

shot1110-1

元に戻すには、ポインタを画面上部に移動させます。すると、メニューも出ますし、緑色のボタンも出てきます。再度緑のボタンを押せば、元に戻ります。他には、「フルスクリーンを解除」のメニュー項目を探すのもいいでしょう。さらに、トラックパッドであれば、3本指スライドで、別のアプリケーションに移動するという方法もあります。もちろん、この方法は、フルスクリーンのままに、別の画面に切り替わります。

shot1111-1

ちなみにMavericksではこうでした。フルスクリーンにしたら、ウインドウの右側に戻すボタン領域が作られていましたが、Yosemiteではそれがなくなりました。

スクリーンショット 2014-07-28 2.50.37

 

[IM]ContributorとSpecial Thanksの定義

ReadmeなどにINTER-MediatorのContributorとSpecial Thanksの項目があって、ともかく英語で書いてありますが、この区分をいちおうきちんと定義しておきます。もちろん、状況に応じてルールの増減はあります。

Contributor

  • コア部分に関連するソースコードをコミットあるいは提供した
  • レポジトリにまとまったサンプルを提供した
  • 開発進行において重要な決定を下し、それを遂行した
  • Webサイトを主体的に編集した。あるいはWebサイトにまとまったページを作成した
  • 貢献順。たとえば、コードの行数など。判断はとりあえず新居が行う

Special Thanks

  • バグレポートを具体的にコミュニティに公開した
  • イベントやサイト運用などのコミュニティ活動を支えた
  • 順番は姓のアルファベット順、Contributorになればこちらのリストからは落とす
  • 通常は氏名のみ、つまり組織名は書かないが、希望があれば書きますよ

こんなところかと思いますが、ご意見ありますでしょうか?

[IM]コンテキストの共有化とPusherの利用

INTER-Mediatorでは、「コンテキスト」は、データベースに対するデータの出入り口的なイメージのものであり、検索条件などでの意味づけされたデータソースを意味します。その「共有化」とは、同一エンティティが複数のページ上のオブジェクトに展開されているとき、1つのエンティティを変更すると、その結果が他のオブジェクトにも反映される仕組みと定義します。Ver.4.4までに、単一ページ内のコンテキストの共有化が実現しています。つまり、あるページ上に、同一フィールドとバインドした要素があるとすると、一方を変更すると、もう一方は自動的に更新します。この動作を実現するためのプログラミングは必要なく、バインドの設定(ターゲット指定の付与)だけで可能です。

Ver.4.5に向けて、コンテキストの共有化をマルチクライアントで実現する仕組みを開発しており、概ね動くところまできました。つまり、同一のページを複数のクライアントで参照しているとき、誰かがデータを変更すると、その結果は他のユーザのページにも反映されるという動作が典型的です。従って、1つのフィールドを単一の要素にバインドしている場合でも、マルチユーザつまり複数のブラウザで同一のエンティティをバインドしているという点で「共有化」されていると言えるわけです。

コンテキストの共有化を実現するために、ページファイル上でのターゲット指定や、定義ファイルでのコンテキスト定義以外に何をしなければならないかをこの文書にまとめておきます。単一ページ内のコンテキストの共有化は特別な仕掛けは不要です。しかしながら、マルチクライアントでのコンテキストの共有化では、WebRTCを利用したPusherというサービスを利用することにしました。試用程度なら無償ですが、実運用には有償となってしまうものの、開発の効率化のために利用することにしました。

Pusherアカウントの取得とアプリケーション登録

Pusherのサイトでアカウントを取得します。Pusherでは「App」という単位で管理ができるので、たとえばINTER-Mediatorで作る1つのソリューションを、1つのPusherのAppとして登録するという方法もありますし、複数のソリューションで共有してもいいかもしれません。いずれにしても、アカウントを作成し、New Appというボタンなどで新たに1つのAppを作成します。ページ上に表示されるapp_id、key、secretの3つの情報がこの後に必要となります。

Pusherのサーバプログラムのインストール

PusherのサーバモジュールはPHP版を利用します。こちらのレポジトリをダウンロードし、そこから得られるlibディレクトリにあるPusher.phpという1つのファイルだけをサーバにインストールします。他は使用しません。ファイルはPHPの設定ファイル(php.iniが代表的)で、include_pathの設定で参照できるディレクトリにあればかまいません。もっとも安直な方法は、INTER-Mediatorフォルダに入れて、サーバにコピーしておくことです。もし、設定が以下のようなものであれば、例えば/usr/lib/phpディレクトリにPusher.phpをコピーしておけば良いでしょう。

include_path = ".:/usr/lib/php/pear:/usr/lib/php"

ページファイルへの追加

Pusherのクライアントソフトウエアを、ページファイルで組み込む必要があります。たとえば、以下のように、ヘッダ部で定義ファイル(include_MySQL.php)の読み込みの前に読み込みます。この方法だと、Pusherのサイトから直接取り出すので、ファイルを自分のサーバにコピーする必要はありません。ソースはこの通りコピペで大丈夫ですが、Pusherのバージョンが変わった時などはそれに合わせてください。

<html>
<head>
    :
    <script src="http://js.pusher.com/2.2/pusher.min.js" type="text/javascript"></script>
    <script type="text/javascript" src="include_MySQL.php"></script>
</head>

定義ファイルあるいはparams.phpへの追加

Pusherで定義したAppに関する指定は、定義ファイルのオプション部あるいはparams.phpで指定をします。原則的にはどちらか一方で定義をしてください。両方指定すると、定義ファイルの方が優先されます。定義ファイルでは、pusherをキーにした配列を定義し、さらにPusherのAppで示された3つの値を配列の各要素の値とします。以下は、定義ファイルでの定義例です。

IM_Entry(
    array( 
              /* コンテキストの定義 */ 
       ),
    array(
        :
        'pusher' => array(
            'app_id' => '1234',
            'key' => '9876543210',
            'secret' => '9876543210',
        ),
    ),
    array('db-class' => 'PDO'),
    false
);

params.phpファイルに記述するときには、以下のように、$pusherParameters変数に同様な配列として定義をします。

$pusherParameters = array(
 'app_id' => '1234',
 'key' => '9876543210',
 'secret' => '9876543210',
);

上記のいずれかがあると、マルチクライアントのコンテキストの共有化がオンになります。定義ファイルあるいはparams.phpの指定の有無だけで、共有化の利用/不使用が決まります。指定がないと一切何も行いません。指定があるのに、Pusherのサーバあるいはクライアントソフトウエアが利用できない状態になると、なんらかのエラーが発生します。

現状での制約

レコードの追加においては、そのコンテキストの検索条件を加味して、検索条件に合わないレコードの追加は行いません。しかしながら、別のクライアントで作成したレコードが当初はコンテキストに合わないものの、フィールドの値を変更してコンテキストの検索条件に合うようになっても、現状ではそのレコードが見えるようにはなりません。

さらに、コンテキストのソート条件は現状では加味されておらず、一連の表示リストのサイトに常に追加されます。

[IM]コンテキストに対応したモデルの実装

以前に「コンテキストの内部実装」という記事を書きましたが、コンテキストというか、正確には、コンテキストに対応したモデルの実装について、書いておきます。内部のソースを読んでいない人にはなんのことかさっぱり分からないと思います。

この仕組みを実装しているのは、INTER-Mediator-Context.jsファイルにあるIMLibContextクラスです。そこにあるいくつかのプロパティのデータ構造は、コードを読む場合や改造では絶対に必要な知識なので、一度まとめておこうと思っていました。

以下、「recKey」は、レコードを特定する文字列で、「キーフィールド=その値」という文字列です。たとえば、”id=3″のような文字列に相当します。そして、以下の「key」はすべてフィールド名の文字列が入ります。「portal」はFileMaker Serverを使う場合に、定義ファイルで’portal’=>trueにしている場合の、関連レコードに対応する外部キーの値です。FMS以外ではportal引数は渡されません。以下、portalが有る場合とない場合のデータ形式を記述します。

まず、IMLibContextクラスのプロパティstoreには、実際のフィールドのデータを記録します。store自体がオブジェクトで、以下のようにアクセスすることで、フィールドの値Valueにアクセス可能です。

this.store[recKey][key]→Value
this.store[recKey][key][portal]→Value

次に、プロパティbindingです。これは、データベースのフィールドが、どの要素に結びついているのかを記録するオブジェクトです。データベースから、バインドしているノードを求めるために用意しています。右辺は、オブジェクトの配列になっています。IdValueはバインドしたノードのid属性値です。Targetは、そこでのターゲット指定です。なお、Target指定は “” の場合もありますが、そのときにはTargetの値は “_im_no_target” という文字列にしています。

this.binding[recKey][field]→[{id: IdValue, target: Target}, ...]
this.binding[recKey][field][portal]→[{id: IdValue, target:Target}, ...]

次はcontextInfoプロパティです。こちらは、バインドしたノードから、データベースのどのレコードの、どのフィールドなのかを求めるためのものです。要するに逆テーブルです。thisと、.contextは同じものですが、thisが使えない状況を考慮してプロパティを作ってあります。

this.contextInfo[nodeId][target].context→コンテキスト自身
this.contextInfo[nodeId][target].record→recKeyの値
this.contextInfo[nodeId][target].field→keyの値
this.contextInfo[nodeId][target].portal→portalの値

これらの3つが主要なデータ構造となります。

FileMaker 13で、XMLおよびXSLTによる出力

久しぶりにタイトルのような事をやりました。この記事のハイライトは、変数でXSLTファイルの位置を指定できるようになったものの、エラーが出るという事象に遭遇しますが、その対処方法が分かったということです。

XML出力は、FileMaker 5くらいからありますし、出力するXMLデータにXSLTを適用して、テキストやらHTMLやらを生成することができるので、ちまちまと文字列処理を書かなくてもいいので便利です。XSLTの習得が大変という話もありますが、今時この種の情報は「基本知識」でしょうから、勉強すればいいのです。私自身は幸いにも、今はなくなってしまったXSLTでのWeb公開の時代に、かなり勉強していたし実稼働させたソリューションもあったので、自分的にはXSLTはOKです。この記事では、XMLやXSLTあるいはそれらを利用するFileMakerの機能については詳細には説明しませんので、必要な方は予習をしてください。

現実にXML/XSLTを使うとき、当然ながらXMLはテーブルを出力するときのフォーマットなので、テーブルから生成されることになります。一方、XSLTはどうでしょうか? 確実は方法は、Webサーバ等に配置して、URLで指定することですが、データベースファイルと別の管理対象ができて不便です。そこで、XSLTのテキスト自体をテーブルのフィールドにテキストとして保存しておくことで、データベースと一体化して管理ができて便利です。ただし、そのXSLTのテキストを利用するときにはファイルにしなければなりません。現実にはスクリプトを組むとして、1フィールド、1レコードだけのタブ区切りで出力すれば、余計な “” などは含まれません。ただし、その1フィールドに、改行が入っていると、コード番号12のものに置き換えられます。後から変更するのは面倒なので、XSLTのテキストから改行を除去する計算式を設定した計算フィールドをエクスポートすればいいのです。もちろん、XSLT内では改行の有無で動作が変わらないにしなければなりません。

さて、そこで、このXSLTファイルをどこに保存するのかということですが、WindowsでもMacで稼働できるようするには、テンポラリフォルダがいいのではないでしょうかということで、次のようなスクリプトで変数にパスを入れることにしました。順当な方法かと思います。

スクリーンショット 2014-06-26 10.47.20

この変数$templateJの結果を利用して、XSLTのテキストをエクスポートします。前に説明したように、スクリプトで、1レコードのみを対象レコードとして、改行を省いたテキストのフィールドだけをタブ区切りで出力します。出力ファイルに、変数$templateJを指定します。

ここで、XSLTによる変換をスクリプトのどこかで組み込むとします。スクリプトステップの「レコードのエクスポート」で、「出力ファイルの指定」を行い、OKボタンを押すと、ファイルタイプが「XML」の場合はさらに次のダイアログボックスが表示されます。ここで「XSLスタイルシートを使用」を選択し、ファイルを指定するわけですが、「ファイル」だとダイアログボックスが出て来て、特定のファイルの指定が必要です。Get ( テンポラリパス ) は一定のパスではない模様ですし、Win/Macでパスは違います。なので、「計算」を選択して、前の変数$templateJを指定して、実際に動かしてみます。ボタンと計算式の文字列が重なっているところにFileMaker社のこの機能への姿勢が見て取れます。

スクリーンショット 2014-06-26 10.46.40

すると、XMLのエクスポートをするときに、次のようなエラーが出ます。SAX(XMLのパーサ)のエラーがそのまま見えていて意味が分かりにくいですが、ようするにファイルが見つからないということです。Windowsでは、実際に見つからなかったパスも見えています。

スクリーンショット 2014-06-26 10.49.33

変数$templateJで出力し、その直後に変数$templateJでファイル参照して見つからない。ようするにバグなのですが、バグで使えません、以上ということでは前に進みません。バグは直らない可能性もあるからです。

そこで、変数$templateJの内容を、MacとWindowsで調べ見ました。Macだと、「/Macintosh HD/var/tmp/….」みたいになっています。Windowsだと、「/C:/Users/msyk/….」となっています。まず、Macは、明らかにパスの最初のコンポーネントを取り去れば、存在可能なUNIXパスになります。Windowsは…ともかく、まず、XSLTのテキストをエクスポートした後、以下のような「変数を設定」スクリプトステップを追加して、最初のコンポーネントを省いてみました。

スクリーンショット 2014-06-26 10.46.58

Macでは、予想通り、それでエラーなく動きました。一方、Windowsは別の対策をしないといけないかと思ったのですが、なんと、ちゃんと動きました。UNIXみたいなパスの与え方でどうやら動くようです。

XSLTのTipsを少し書いておきましょう。関連レコードのフィールドは、TOさえあれば出力できると思っていたら、ポータルに展開しないと出力されません。ポータルがない場合は、ちゃんと警告メッセージを出して、最初のレコードの値だけが出力されると言われます。ということで、ポータル作成はさぼれません。

関連レコードのデータは、たとえば2つのフィールドがあるとして、関連レコードが3つあると、次のように出力します。このノードの上位ノードのタグはROWになります。

<COL><DATA>1</DATA><DATA>2</DATA><DATA>3</DATA></COL>
<COL><DATA>abc</DATA><DATA>def</DATA><DATA>ghi</DATA></COL>

上記の2つのフィールドの番号を1、2とします。ROWタグがカレントノードのとき、上記のデータを展開するのにfor-eachを使うのですが、同じポータルにある関連レコードがいくつかるとき、for-eachをそのうち1つのフィールドに対して行います。次のような感じです。ネームスペースの略号は、xsl、fmrとしています。

<xsl:for-each select="fmr:COL[1]/fmr:DATA">
    <div>
        <xsl:value-of select="." />
        <xsl:value-of select="../../fmr:COL[2]/fmr:DATA[position()]" />
    </div>
</xsl:for-each>

最初のfor-eachは普通にDATAタグが複数あるので、それを1つ1つ取り出します。3行目の「.」によって、1つ目のフィールドの現在のDATAのテキストを取り出すので、「1」「2」「3」という値が順次取り出されます。2つ目のフィールドは、4行目にあるように、XPATHの記述で、カレントノードからROWまで「../..」でさかのぼり、そこから、COL、DATA集合へと下ります。そして「position()」というのは関数です。for-eachでループしているときに、何番目なのかを示す値を返します。これで、「abc」「def」「ghi」という文字列が順次得られます。

最後に大サービスで(笑)、FileMakerのXML出力向け(ただし、FMRXMLRESULTスキーマ)の汎用的なテンプレートを書いておきましょう。コピペして、あとはデータの順序に合わせてFMPXMLRESULTやROWタグ要素に対応するテンプレートにHTML等で出力テンプレートを書くだけです。

<?xml version="1.0" encoding="UTF-8"?>

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:xhtml="http://www.w3.org/1999/xhtml"
    xmlns:fmr="http://www.filemaker.com/fmpxmlresult"
    exclude-result-prefixes="xhtml fmr">
<xsl:output encoding="UTF-8" method="xml" indent="yes"
   omit-xml-declaration="yes"/>

<xsl:template match="fmr:ERRORCODE | fmr:PRODUCT | fmr:METADATA">
</xsl:template>

<xsl:template match="fmr:FMPXMLRESULT">
    <html><body><table>
       <tr><th>Name</th><th>Activity</th></tr>
       <xsl:apply-templates />
    </table></body></html>
</xsl:template>

<xsl:template match="fmr:ROW">
    <tr>
        <td><xsl:value-of select="fmr:COL[3]/fmr:DATA[position()]" /></td>
        <td>
            <table>
                <xsl:for-each select="fmr:COL[1]/fmr:DATA">
                   <tr>
                       <td><xsl:value-of select="." /></td>
                       <td><xsl:value-of select="../../fmr:COL[2]/fmr:DATA[position()]" /></td>
                   </tr>
                </xsl:for-each>
             </table>
         </td>
    </tr>
</xsl:template>