2017-05-29

Laravel と Google カレンダーを自動で同期

現在の開発で、とある日付付きのデータが追加/変更/削除されたら Google Calendar へデータを反映させるという機能を追加しました。
正直 Google Calendar API の部分でハマって時間がかかってしまったので、今後のためにもこの記事で Laravel側のプログラムも含めてまとめておこうと思います。


[開発環境]
Laravel: 5.4

[やりたいこと]
日付がついたデータがデータベースに{追加/変更/削除}されたら自動で Google Calendar のイベントを同じく{追加/変更/削除}する。
つまり、DB内のデータと Google Calendar のイベントデータの同期がしたいわけです。
そしてカレンダーをウェブページで表示する。


[手順]
では、大まかな手順です。

1.Google Calendar API を使って読み込み、書き込み、削除ができるよう設定する

2.Laravel に自動実行用のイベント を設定。

3.カレンダーを表示


1.Google Calendar API を使って読み込み、書き込み、削除ができるよう設定する

最初に必要となるのは API と、これから自動的に操作するカレンダーの設定です。
正直、昔のGoogle API それほど複雑ではなく API KEY を取得して URL に含めるだけでOKでしたが、久しぶりに見たらいろんなアプローチの仕方があって困惑してしまいました。
セキュリティ上が大きな原因でしょうから仕方ないといえば仕方ないですね。

ということで、 手順です。

(1) 以下のリンクから Google API でプロジェクトを作成し、サービスアカウントを取得します。

https://console.developers.google.com/permissions/serviceaccounts

やり方は動画を参照してください。
(設定が完了すると json ファイルが自動でダウンロードが開始になるので適当な場所に保存しておいてください。)
↓↓↓



そしてカレンダーAPI を有効に設定しましょう。
↓↓↓



最後にカレンダーの方にカレンダーの書き込み権限を付与します。
そのためにはサービスアカウントを取得した際にダウンロードした json の中に書かれているメールアドレスが必要になるのでテキストエディタなどでファイルを開いてコピーしておきましょう。

"client_email": "********@************************.iam.gserviceaccount.com",

ではカレンダー内での設定です。
↓↓↓



モザイクをかけているので少しわかりづらいかもしれませんが、先ほどダウンロードした json ファイルの中の "client_email" にかかれているメールアドレスを入力しています。
つまり、カレンダーの権限を API に付与しているわけですね。


さて、これで Google API 側の設定は完了です。
次からは Larave 側への設定になります。


2.Laravel に自動実行用のイベント を設定。

準備1: 実行用のイベントを作る前に Google API を操作できるパッケージをインストールしておきましょう。

composer require google/apiclient:^2.0

準備2: ダウンロードした json ファイルを /storage/json/google_api_secret_key.json として設置します。

準備3:カレンダーID を以下のように .env に追加します。

GOOGLE_CALENDAR_ID=****************@group.calendar.google.com

※カレンダー ID の取得は以下の動画を参照してください。



さぁ、ここからは実際にプログラムコードを書いていく作業になります。
(ここでもう作業の 50% は完了しています。)


では、最初に Google API のインスタンスを作成しましょう。
 $key_path = storage_path('app/json/google_api_secret_key.json');  
 putenv('GOOGLE_APPLICATION_CREDENTIALS='. $key_path);  
 $client = new \Google_Client();  
 $client->useApplicationDefaultCredentials();  

次にカレンダー API へアクセスできるよう以下のように設定をします。 
 $client->addScope(\Google_Service_Calendar::CALENDAR);  
 $service = new \Google_Service_Calendar($client);  
はい。これで API へアクセスする準備は整いました。
ここからはカレンダーイベントの追加、編集、削除について説明をしていきます。
※これは実際の実装ではなくイベント操作の基本編です。
実装については「Laravel の Modelイベント」まで読み飛ばしてください。

1.カレンダーイベントの追加

イベントの追加は insert() を使います。
 $calendarId = env('GOOGLE_CALENDAR_ID');  
 $event = new \Google_Service_Calendar_Event([  
   'summary' => 'テストイベントの投稿',  
   'location' => '東京都墨田区押上1丁目1−2',  
   'description' => '東京スカイツリー・バンジージャンプイベント!',  
   'start' => [  
     'dateTime' => '2017-05-16T09:00:00-07:00',  
     'timeZone' => 'Asia/Tokyo',  
   ],  
   'end' => [  
     'dateTime' => '2017-05-16T17:00:00-07:00',  
     'timeZone' => 'Asia/Tokyo',  
   ]  
 ]);  
 $new_event = $service->events->insert($calendarId, $event);  
$calendarId は .env に設定したカレンダーIDになります。
上の例では一日だけのイベントですが2日連続、3日連続などの設定も可能です。
その際は本家のリファレンスページを参照してください。

ちなみに $new_event->getId(); 
とするとイベントIDが取得できるので、このIDをDBなどに保存しておくことでこのイベントの変更、削除をすることができます。


2.カレンダーイベントの変更
 $calendarId = env('GOOGLE_CALENDAR_ID');  
 $event_id = '******************';  
 $event = $service->events->get($calendarId, $event_id);  
 $event->setSummary('変更されたイベント');  
 $service->events->update($calendarId, $event_id, $event);  
さきほどのイベントIDを使って該当するイベントの内容を変更できます。
summary だけでなくその他の情報も変更できます。リファレンスを参照してください。 

※ちなみに変更されたイベント名は以下のように取得します。
 echo $event->getDescription();  

3.カレンダーイベントの削除
 $calendarId = env('GOOGLE_CALENDAR_ID');  
 $event_id = '******************';  
 $service->events->delete($calendarId, $event_id);  
これもカレンダーID と イベントID を指定することで削除が可能です。

さぁ、ここまでがカレンダーイベントを操作する基本になります。
ここからは Laravel の Modelイベントでの実装(つまり実践編)です。

Laravel の Modelイベント

1.Modelの追加イベント

コマンドラインから起動するイベントファイルを作成しましょう。
以下のようにします。

php artisan make:event ItemCreated

これで app/Events フォルダに ItemCreated.php というファイルが作成されているかと思います。

では中身を開いて実際にイベントを追加してみましょう。 
 public function __construct(Item $item)  
 {  
   $calendarId = env('GOOGLE_CALENDAR_ID');  
   $event = new \Google_Service_Calendar_Event([  
     'summary' => $item->summary,  
     'location' => $item->location,  
     'description' => $item->description,  
     'start' => [  
       'dateTime' => $item->date_start,  
       'timeZone' => 'Asia/Tokyo',  
     ],  
     'end' => [  
       'dateTime' => $item->date_end,  
       'timeZone' => 'Asia/Tokyo',  
     ]  
   ]);  
   $service->events->insert($calendarId, $event);  
 }   
これでイベントのプログラム自体は完了ですが、このイベントを起動するプログラムが準備できていません。
しかし、さすがの Laravel です。起動は Model に trait と変数を指定するだけで完了します。
以下のように設定しましょう。
 class Order extends Model  
 {  
   use Notifiable;  
   /**  
    * The event map for the model.  
    *  
    * @var array  
    */  
   protected $events = [  
     'created' => ItemCreated::class  
   ];  
これで、Item テーブルにデータが追加されたら自動で ItemCreated が呼ばれ、その中でイベントがカレンダーに追加されることになります。
このようにイベントを設定しておくとどこでデータを操作をしても忘れずにプログラムを実行してくれるのでとても便利ですね。
 
2.Modelの変更イベント

基本的には追加と代わりませんので簡単な説明だけです。

php artisan make:event ItemUpdated

 public function __construct(Item $item)  
 {  
   $calendarId = env('GOOGLE_CALENDAR_ID');  
   $event = $service->events->get($calendarId, $item->event_id);  
   $event->setSummary('変更されたイベント');  
   $service->events->update($calendarId, $item->event_id, $event);  
 }   
   /**  
    * The event map for the model.  
    *  
    * @var array  
    */  
   protected $events = [  
     'updated' => ItemUpdated::class  
   ];  

3.Modelの変更イベント
 
削除も同じです。
 
php artisan make:event ItemDeleted
 
 public function __construct(Item $item)  
 {  
   $calendarId = env('GOOGLE_CALENDAR_ID');  
   $service->events->delete($calendarId, $item->event_id);   
 }   
   /**  
    * The event map for the model.  
    *  
    * @var array  
    */  
   protected $events = [  
     'deleted' => ItemDeleted::class  
   ];  


はい。
これでModelイベントを利用してカレンダー・イベントを操作してみました。
 
ちなみにもしイベントを好きなタイミングで起動したい場合は以下のようにします。

 event(new ItemCreated($item));  
 event(new ItemUpdated($item));  
 event(new ItemDeleted($item));  



この辺も Lravel のいたれりつくせり感があふれています。
 


3.カレンダーを表示

 
さぁ、せっかくここまでイベントを設定してきたので、自動操作されるカレンダーを実際にウェブページに表示してみましょう。
とはいっても、Google カレンダーでは HTMLタグを提供してくれているのでとても簡単に実装することができます。
 
やりかたは、カレンダーIDを取得したページの「このカレンダーを埋め込む」の項目にある HTMLタグをコピー&ペーストするだけです。

DBにデータを追加/編集/削除するとリアルタイムで(少し時間がかかる場合もあります)変更されると思います。
 
 
以上、今回は Google API を使ってデータを同期する方法でした。
実現するには少し時間がかかるかもしれませんがとても便利な機能なのでぜひチャレンジしてみてください。
 
ではでは! 

2017-05-22

Vue + jQuery の弱点に初めて遭遇!

このところ Vue.js を使って開発効率が格段に上がってきているのを感じています。しかも以前の Vue.js 2.0 を使ってみた9つの感想 でも取り上げましたが、jQuery との衝突が全く無かったので機嫌よく Vue での開発を進めていたのですが、今回初めて衝突というか弱点に遭遇したのでまとめと対処法をお届けしたいと思います。



まずは遭遇した状況から。
jQuery(Bootstrap) を使ったページ内で JavaScript を使って input 内の値を変更しようとしました。(※詳しく言うと、後でも言及する datepicker を使ってテキスト内に日付を入力しようとしてました。また、冗長になるので、 this.input = 'xxxxxx'; を使うのは無しの方向です。)

私の頭の中では以下のように通常通り $('#id').val('xxxxx'); を使ってテキストの中身を変更すれば Vue の方でもデータが更新されるものだと思っていました。
しかし、以下のデモのようにテキスト内は変更できるものの、Vue の方では変更は全くないという状況でした。

Vue + jQuery の change イベント実験 - 1



、、、なぜだろう。
jQuery の val() でデータを変更しても change イベントは呼ばれないというのは知っていました(参考ページ)が、今回使っているのは vue の v-model なので問題はないはずなのに・・・?


そこで少し stackoverflow を探ってみると、vue には $forceUpdate() という強制的にデータを更新する方法があるよ、ということなので以下のページのように実際に試してみました。

Vue + jQuery の change イベント実験 - 2


でも、これもうまくいかない、、、、
クリックを続けているとたまに自動入力されるテキストがちらつくことがあるので、おそらく変更はされたが、$forceUpdate() によって瞬間的に元にもどされてしまっているという状況なのだろう(つまり Vue には伝わっていない)と思いました。


またしてもスタート地点に戻ってしまったので、もう一度いろいろとネット上の情報を探ってみると本家 GitHub の Issue で手がかり見つけました。

Triggering vuejs on programatical change of element.


どうやら、jQuery の changeイベントはネイティブの JavaScript のイベントとは別のものなので、もし Vue の変更をやりたいならネイティブイベントを作って dispatch しないといけないということでした。

そこで、早速以下のようにイベント送出をしてみました。

Vue + jQuery の change イベント実験 - 3


おっ、いけました!
この状態だと直接テキスト入力してもボタンをクリックしても Vue のデータが書き換わるのでリアルタイムに上のテキストも変更されるようになっています。

へぇ、Vue にはネイティブ JS のイベントが必要だったんですね。


では、本題の datepicker を使うにはどうすればいいのでしょうか。
今回は jQuery の changeイベントとのコラボでやってみました。

$('.datepicker').datepicker().on('change', function() {

    var event = document.createEvent('HTMLEvents');
    event.initEvent('input', true, true);
    $(this).get(0).dispatchEvent(event);

});

実際のテストは以下です。

Vue + jQuery の change イベント実験 - 4


やってみたら分かっていただけると思いますが、テキスト入力(数字しか入力できません)でもカレンダー選択のどちらでも Vue へデータが伝わっていると思います。

※ちなみに送出するイベントが「input」ではなく「change」の場合だと、これもうまくいきませんでした。

ということは、この方法を使えば「dispach-native-event」などのクラス名に changeイベントを作っておき、その中でネイティブ・イベントを送出することができるので、サイト全体での対処も比較的楽にできるかと思います。


んー、それにしてもこんな形で Vue + jQuery の問題点に遭遇するとは思いませんでした。
でもその他の部分では問題もなく、開発速度も上がり、保守もしやすいと思うので Vue の利用は続けていこうと思います。


【あとがき】

フロントエンド界の活動は近年ホントに活発でそれ自体はとてもいいことなのですが、正直なところをいうとたくさんありすぎてもう少し集約してほしい気はしています。
また、複雑さが年を追うごとにひどくなってきていて、「あれ、なんのためのフレームワークだったけ?開発効率、ほんとにこれで上がってる??」なんてことになってきているなぁ、と pythonプログラミングをしているとよく感じます。

この間どこかのブログで「3年後も react 使ってると思う? vue 使ってると思う? でも確実に jQuery は使ってるよね?」という趣旨の記事を読みました。

これには Vue が大好きな僕でも「確かにそれはあるかもー」と思いました。
そんなこんなもあるので、変化の速い(早すぎる)これからのプログラミングは学習コストの低さも重要なファクターの一つになってくるんじゃないかな、と今日出してきた扇風機にあたりながら考えてました。
(もちろん何を専門にするかで変わってくるでしょうけどね。)


それはともかく、今年の夏は涼しかったらいいなー(笑)




2017-05-09

Python でバーコードをスキャン(ソースコード・ダウンロード可)

今回は、たくさんの商品画像の中にあるバーコードの内容をプログラムでスキャンして取得&その写真の分類ができるようにしてみたいと思います。
利用するのは次の3つです。

1.Python(2.7.12)
2.OpenCV2
3.Zbar

※実際のコードは GitHub からダウンロードできます。


【準備】

まずバーコードをスキャンするには zbar というパッケージが必要になりますのでインストールをしましょう。Ubuntu だと以下のコマンドで zbar をインストールできます。
 sudo apt-get install libzbar-dev  
そして、python から zbar が使えるようにするため pip でインストールです。
 pip install zbar  
はい。
これでインストールは完了です。(OpenCVはメジャーなのでインストールは省きます。)

【基本編】

では、zbar を使って画像の中にあるバーコードをスキャンする簡単なコードを作っていきましょう。
 import cv2  
 import zbar  

 scanner = zbar.ImageScanner()  
 scanner.parse_config('enable')  
必要なパッケージをインポートして、zbar のスキャナーを作成します。

 im = cv2.imread('images/barcode1.jpg')  
 gray_im = cv2.cvtColor(im, cv2.COLOR_BGR2GRAY)  
OpenCV でスキャンしたい画像を読み込んでグレースケールに変換します。

 rows,cols = im.shape[:2]  
 image = zbar.Image(cols, rows, 'Y800', gray_im.tostring())  
 scanner.scan(image)  
ここで zbar の image を作成します。
1行目で画像のサイズとさっき作った gray_im をセットして実際にスキャンを開始します。

 for symbol in image:  
   print 'Type: %s, Data: %s' % (symbol.type, symbol.data)  
スキャンした結果を表示します。
type には 「qrcode」や「isbn10」などのデータタイプ、そして data には読み取ることができた値が入っています。

以上が簡単な例でした。
ただ、この例ではバーコードが斜めになっている場合は、検出できないことがあります。
そこで、次の応用編では斜めになったバーコードにも対応できるようにしてみましょう。

【応用編】

 import cv2  
 import zbar  

 scanner = zbar.ImageScanner()
 scanner.parse_config('enable')

 im = cv2.imread('images/barcode2.jpg')  
 gray_im = cv2.cvtColor(im, cv2.COLOR_BGR2GRAY)  
 rows,cols = im.shape[:2]  
ここまでは基本編のおさらいです。
(ちなみに barcode2.jpg ではわざとバーコードがななめになっています。)

では、ここから斜めになったバーコードに対応するため輪郭を取得していきましょう。
 ret,threshold_im = cv2.threshold(gray_im, 150, 255, cv2.THRESH_BINARY)  
 im,contours,hierarchy = cv2.findContours(threshold_im, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)  
まず、しきい値を使って画像を白と黒だけに変換します。
そして、findContours() で輪郭を取得します。

 for contour in contours:  
   rect = cv2.minAreaRect(contour)  
   center_pt = (int(rect[0][0]), int(rect[0][1]))
   w = int(rect[1][0])  
   h = int(rect[1][1])  
   angle = int(rect[2])
次に、取得した contours を for ループで回して一つ一つの長方形を取得しましょう。
minAreaRect がその部分になります。

rect は 中心座標、横幅、高さ、傾きのデータを持っているのでこれらをわかりやすいようにひとつひとつ変数(center_pt, w, h, angle)に格納していきます。

 M = cv2.getRotationMatrix2D(center_pt, angle, 1)
 rotated_im = cv2.warpAffine(im.copy(), M, (cols,rows))  
長方形の傾き(角度)が分かったのでこれを使って画像を回転させましょう。
一行目は行列を作って2行目で回転させた画像を取得しています。

 zbar_image = zbar.Image(cols, rows, 'Y800', rotated_im.tostring())  
 scanner.scan(zbar_image)  
そして、さっきと同じく zbar の image をつくります。

 for symbol in zbar_image:  
     symbol_type = symbol.type  
     symbol_data = symbol.data  
       if(symbol_type not in scanned_data.keys()):  
           scanned_data[symbol_type] = []  
           if symbol_data not in scanned_data[symbol_type]:  
               scanned_data[symbol_type].append(symbol_data)  
for ループで回して、もし scanned_data に type 別の値が入っていなければデータを格納。(←つまり、重複防止ですね)

これで、 scanned_data の中にスキャンされたバーコードの情報が入っていることになります。
あとは MySQL や JSON にデータを格納するなどして分類結果を保存するなどすればいいでしょう。

今回は以上です。