Djangoによるスマホアプリバックエンドの実装

97
Djangoによる スマホアプリ バックエンドの実装 PyCon JP 2014

description

PyCon JP 2014でセッションさせて頂いた「Djangoによるスマホアプリバックエンドの実装」のスライドです。

Transcript of Djangoによるスマホアプリバックエンドの実装

Page 1: Djangoによるスマホアプリバックエンドの実装

Djangoによる スマホアプリ バックエンドの実装

PyCon JP 2014

Page 2: Djangoによるスマホアプリバックエンドの実装

Yuichi Nakazawa

@y_nakazawa1220

(株)日本システム技研 所属

スマホアプリのバックエンド開発

  GEEKLAB.NAGANO 管理人

自己紹介

Page 3: Djangoによるスマホアプリバックエンドの実装

Kazuhiko Kakita

@kaki_k

(株)日本システム技研 所属

スマホアプリのバックエンド開発

  GEEKLAB.NAGANO 管理人

自己紹介

Page 4: Djangoによるスマホアプリバックエンドの実装

株式会社 日本システム技研

http://jsl.co.jp

Page 5: Djangoによるスマホアプリバックエンドの実装

キャスタリア株式会社【goocus pro】

”モバイル&ソーシャル”をコンセプトに設計された、”Mobile Native”なラーニングプラットフォーム『goocus pro』

『B2B』SaaS型のサービスとして、企業様・学校様等にご提供

教育ビジネスを展開される企業様にプラットフォームとしてご提供

『B2B2B』『B2B2C』

プログラミング学習が必修の通信制高等学校「コードアカデミー高等学校」を設立しました

『「ソーシャルラーニング」入門 ソーシャルメディアがもたらす人と組織の知識革命』の翻訳を手掛けました

国内外の先端的な教育/学習の最新情報をお届けするブログを運営しています

日本オープンオンライン教育推進協議会『JMOOC』に正会員として参加していますhttp://www.castalia.co.jp

Page 6: Djangoによるスマホアプリバックエンドの実装

GEEKLAB. NAGANO

http://geeklab-nagano.com

Page 7: Djangoによるスマホアプリバックエンドの実装

GEEKLAB. NAGANO

GEEKLAB. NAGANOとは

• 地元のエンジニアを集めて勉強会・セミナーを開催

• 知識・ノウハウの集積基地

• 長野からのITの発信を!!

Page 8: Djangoによるスマホアプリバックエンドの実装

設備(全部無料です!)

GEEKLAB. NAGANO

椅子

テーブル ソファー

単焦点プロジェクター

非破壊スキャナ

ホワイトボード

IT書籍、雑誌

インターネット接続(WiFi, 有線)

Apple TV

電子工作機器

自販機

スライム・・

Page 9: Djangoによるスマホアプリバックエンドの実装

GEEKLAB. NAGANO

• 利用可能日時:平日は9-18時頃(勉強会・セミナー以外)。土日祝日は問い合わせ要

• 運営:                             学校法人 信学会株式会社日本システム技研(JSL)キャスタリア株式会社

利用時間・運営

Page 10: Djangoによるスマホアプリバックエンドの実装

長野に来られた際は、是非お立ち寄りを!!

Page 11: Djangoによるスマホアプリバックエンドの実装

• モバイルファースト!

• スマートフォンと連携したバックエンド開発が沢山出てくる時代

はじめに

Page 12: Djangoによるスマホアプリバックエンドの実装

Djangoでバックエンドを作ろう!

Page 13: Djangoによるスマホアプリバックエンドの実装

• 学習コストが低い

• フルスタックのフレームワーク

• scaffoldは無いけど、管理サイトが秀逸

Djangoのメリット

Page 14: Djangoによるスマホアプリバックエンドの実装

管理サイト ログイン画面

Page 15: Djangoによるスマホアプリバックエンドの実装

管理サイト Model選択

Page 16: Djangoによるスマホアプリバックエンドの実装

管理サイト 一覧画面

Page 17: Djangoによるスマホアプリバックエンドの実装

管理サイト 追加画面

Page 18: Djangoによるスマホアプリバックエンドの実装

• 管理サイト + API < 最低限これだけあればOK

• 管理サイト + CMS + API < ここまであれば完璧

アプリケーションの形態

Page 19: Djangoによるスマホアプリバックエンドの実装

CMS部分を作る

Page 20: Djangoによるスマホアプリバックエンドの実装

• 基本となる親子関係のモデルを作る

モデル定義

たとえば、このようなモデル

書籍

感想

1:多

Page 21: Djangoによるスマホアプリバックエンドの実装

# -*- coding: utf-8 -*-from django.db import models!class Book(models.Model): '''書籍''' name = models.CharField(u'書籍名', max_length=255) publisher = models.CharField(u'出版社', max_length=255, blank=True) page = models.IntegerField(u'ページ数', blank=True, default=0) def __str__(self): # Python2: def __unicode__(self): return self.name class Impression(models.Model): '''感想''' book = models.ForeignKey(Book, verbose_name=u'書籍', related_name='impressions') comment = models.TextField(u'コメント', blank=True) def __str__(self): # Python2: def __unicode__(self): return self.comment

models.py の例

•models.ForeignKeyがみそ • これがDBの定義となり、CREATE TABLE文はDjangoが作ってくれる

Page 22: Djangoによるスマホアプリバックエンドの実装

• Djangoに用意されているORMのみでDBアクセスする • ほとんどSQLは書かなくて済む

!def book_list(request): '''書籍の一覧''' books = Book.objects.all().order_by('id') # 親の書籍を全件読む return render_to_response('cms/book_list.html', # 使用するテンプレート {'books': books}, # テンプレートに渡すデータ context_instance=RequestContext(request))!def impression_list(request, book_id): '''感想の一覧''' book = get_object_or_404(Book, pk=book_id) # 親の書籍を1件読む impressions = book.impressions.all().order_by('id') # 書籍の子供の、感想を読む : :!

親の読み方、子の読み方

ORM (Object Relation Mapping)

Page 23: Djangoによるスマホアプリバックエンドの実装

ORMのリレーションで出来ること

1.多対一のリレーション ForeignKey

Manufacturer

Car

1:多

2.多対多のリレーション ManyToManyField

Topping Pizza

多:多

再帰的リレーション (自分自身に対する多対一のリレーション) も可

再帰的リレーション (自分自身に対する多対多のリレーション) も可

(中間モデル)

中間モデルは、DB上に隠しテーブルができるが、 意識しなくてよい

Page 24: Djangoによるスマホアプリバックエンドの実装

3.エクストラフィールドで多対多のリレーション ManyToManyField の through 引数

Person Group

多:多

Membership

中間モデルに項目を持たせて、自分で定義したい場合

4.一対一のリレーション OneToOneField

Restaurant

Place

1:1

モデルを継承して項目追加する代わりに OneToOneField で項目追加したモデルを作る !継承ができないかというと、そうではない

ORMのリレーションで出来ること

Page 25: Djangoによるスマホアプリバックエンドの実装

モデルの継承

1.抽象ベースクラス

CommonInfo

Student

継承

親は実体を持たない

class CommonInfo(models.Model): class Meta: abstract = True

2.マルチテーブル継承

Place

Restaurant

継承

親も子も実体を持つ

class Place(models.Model):

class Student(CommonInfo): class Restaurant(Place):

3.プロキシモデル

User

MyUser

継承子は実体を持たない 子は項目追加できない 親のメソッドを拡張したい時

from django.contrib.auth.models import User

class MyUser(User): class Meta: proxy = True ! def do_something(self): ...

Page 26: Djangoによるスマホアプリバックエンドの実装

一般化リレーション

1.一般化リレーション

User

TaggedItem

一般化リレーション (Generic Relations) または、 多態性リレーション (Polymorphic Relations) とも呼ばれる !これだけ、公式ドキュメントで離れた場所にあって、気付きにくいが、ORMでできることの1つ en: https://docs.djangoproject.com/en/1.6/ref/contrib/contenttypes/#id1 jp: http://docs.djangoproject.jp/en/latest/ref/contrib/contenttypes.html#generic-relations

class TaggedItem(models.Model): tag = models.SlugField() content_type = models.ForeignKey(ContentType) object_id = models.PositiveIntegerField() content_object = generic.GenericForeignKey('content_type', 'object_id')

class Bookmark(models.Model): url = models.URLField() tags = generic.GenericRelation(TaggedItem)

Bookmark

色々な親モデルにタグを付けたい場合など

逆参照しなければ tags = generic.GenericRelation(TaggedItem) は不要

Page 27: Djangoによるスマホアプリバックエンドの実装

• 様々なリレーションの作り方をご紹介しました。

• これらを駆使してモデル図を設計すれば、作りたいデータベースのモデル定義ができると思います。

ORMまとめ

Page 28: Djangoによるスマホアプリバックエンドの実装

• アグリゲーション(Aggregation 集約)

• 意地でもSQLを書かないために、これを極めることが大切

• パフォーマンスを出す上でも、読んで回すロジックではなく、SQL一発に変換されるよう、集約をとことん使う

アグリゲーションを使いこなす

# 最も高額な書籍>>> from django.db.models import Max>>> Book.objects.all().aggregate(Max('price')){'price__max': Decimal('81.20')}!# 出版社ごとの書籍数を "num_books"属性で>>> from django.db.models import Count>>> pubs = Publisher.objects.annotate(num_books=Count('book'))>>> pubs[<Publisher BaloneyPress>, <Publisher SalamiPress>, ...]>>> pubs[0].num_books73

Page 29: Djangoによるスマホアプリバックエンドの実装

• Django 1.6まではSouth - http://south.aeracode.org

• Django 1.7からは標準として取り込まれた

• モデル変更が楽

• model.py の定義変更をDBに反映させることができる

• modelを直すとmigrateファイルを作ってくれる(某フレームワークとは逆)

DB migration

Page 30: Djangoによるスマホアプリバックエンドの実装

Django 1.7からは半強制になった?

アプリケーションを作成する

$ python manage.py startapp myapp

この時、myapp/migrations/__init__.py ができる これがあるとmigation対象、消すと対象外になる

1.6のチュートリアルを見ると、初回の syncdb はなくなって 以下の2つのコマンドに分かれた

$ python manage.py migrate # スーパーユーザーは作られない$ python manage.py createsuperuser # 作りたい場合は、任意で実行

DB migration

Page 31: Djangoによるスマホアプリバックエンドの実装

DB migration

class Book(models.Model): : page = models.IntegerField(u'ページ数', blank=True, default=0)

新たなアプリケーションを作って、models.pyを書いた初回

Page 32: Djangoによるスマホアプリバックエンドの実装

DB migration

$ python manage.py makemigrations myapp

makemigrationsコマンド(models.pyの変更を拾う)

makemigrationsが作成したマイグレーション ファイルを確認myproj/myapp/migrations/0001_initial.py

migrateコマンドで、変更をDBに反映する$ python manage.py migrate myapp

などといったファイルができているので、エディタで確認する

新たなモデルがテーブルとしてDBに作成される

Page 33: Djangoによるスマホアプリバックエンドの実装

DB migration

models.py に isbn という項目を追加したとするclass Book(models.Model): : page = models.IntegerField(u'ページ数', blank=True, default=0) isbn = models.CharField(u'ISBN', max_length=255, blank=True, null=True) # 追加

ここから日常の作業として、

Page 34: Djangoによるスマホアプリバックエンドの実装

DB migration

$ python manage.py makemigrations myapp

makemigrationsコマンド(models.pyの変更を拾う)

makemigrationsが作成したマイグレーション ファイルを確認myproj/myapp/migrations/0002_book_isbn.py

migrateコマンドで、変更をDBに反映する$ python manage.py migrate myapp

などといったファイルができているので、エディタで確認する

モデルの項目追加/変更がDBのテーブルに反映される

Page 35: Djangoによるスマホアプリバックエンドの実装

• CSSフレームワーク http://getbootstrap.com/ • エンジニアだけで作っても見栄えを良くする

Bootstrapを使う

Page 36: Djangoによるスマホアプリバックエンドの実装

• Djangoのテンプレートは継承できるので、以下のように

Bootstrap

BootstrapのJS、CSS を定義したベース

Navbar ヘッダーのナビバー

Navbarを使わないもの ログイン など

Navbarを使うもの CMSの各種ページ

base.html

base_navi.html

index.html などlogin.html など

Page 37: Djangoによるスマホアプリバックエンドの実装

• 一覧系のページは、Bootstrapのclassを使って普通に書く

• フォーム系のページは、django-bootstrap-form https://github.com/tzangms/django-bootstrap-form を使う

Bootstrap

使い方としては

$ pip install django-bootstrap-form

Page 38: Djangoによるスマホアプリバックエンドの実装

{% load staticfiles %}<!DOCTYPE html><html lang="{{ LANGUAGE_CODE|default:"en-us" }}"><head><meta charset="UTF-8"><title>{% block title %}Title{% endblock %}</title><meta name="viewport" content="width=device-width, initial-scale=1.0"><link href="{% static 'css/bootstrap.min.css' %}" rel="stylesheet"><link href="{% static 'css/bootstrap-theme.min.css' %}" rel="stylesheet"><script src="{% static 'js/jquery-1.11.1.min.js' %}"></script><script src="{% static 'js/bootstrap.min.js' %}"></script>{% block extrahead %}{% endblock %}</head><body> {% block navbar %}{% endblock %} <div class="container"> {% block content %} {{ content }} {% endblock %} </div></body></html>

base.html

Bootstrapの例

Bootstrap の JS、CSSを記述する ベースとなるテンプレート

Page 39: Djangoによるスマホアプリバックエンドの実装

{% extends "base.html" %}!{% block navbar %}<nav class="navbar navbar-default" role="navigation"> <div class="container-fluid"> <!-- Brand and toggle get grouped for better mobile display --> <div class="navbar-header"> <button type="button" class="navbar-toggle" data-toggle=“collapse” data-target="#bs-example-navbar-collapse-1"> <span class="sr-only">Toggle navigation</span> <span class="icon-bar"></span> <span class="icon-bar"></span> <span class="icon-bar"></span> </button> <a class="navbar-brand" href="{% url 'mock:index' %}”>Brand name</a> </div> <!-- Collect the nav links, forms, and other content for toggling --> <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1"> <ul class="nav navbar-nav navbar-right"> <li class="dropdown"> <a href="#" class="dropdown-toggle" data-toggle="dropdown">{{ user.username }} <span class="caret"></span></a> <ul class="dropdown-menu" role="menu"> <li><a href="{% url 'logout' %}">Log out</a></li> </ul> </li> </ul> </div> </div></nav>{% endblock %}

base_navi.html

Bootstrapの例

Bootstrap の Navbar のみを定義

← base.html を継承← base.html の navbar ブロックを置き換え

Page 40: Djangoによるスマホアプリバックエンドの実装

{% extends “base_navi.html" %}!{% block title %}書籍の一覧{% endblock title %}!{% block content %} <h3 class="page-header">書籍の一覧</h3> <a href="{% url 'cms:book_add' %}" class="btn btn-default btn-sm">追加</a> <table class="table table-striped table-bordered"> <thead> <tr> <th>ID</th> <th>書籍名</th> <th>操作</th> </tr> </thead> <tbody> {% for book in books %} <tr> <td>{{ book.id }}</td> <td>{{ book.name }}</td> <td> <a href="{% url 'cms:book_mod' book_id=book.id %}" class="btn btn-default btn-sm">修正</a> <a href="{% url 'cms:book_del' book_id=book.id %}" class="btn btn-default btn-sm">削除</a> </td> </tr> {% endfor %} </tbody> </table>{% endblock content %}

index.html

Bootstrapの例

↑ 一覧系は Bootstrap の class を使って普通に書く

← base_navi.html を継承

← base.html の title ブロックを置き換え← base.html の content ブロックを置き換え

Page 41: Djangoによるスマホアプリバックエンドの実装

{% extends “base_navi.html" %}{% load bootstrap %}!{% block title %}書籍の編集{% endblock title %}!{% block content %} <h3 class="page-header">書籍の編集</h3> {% if book_id %} <form action="{% url 'cms:book_mod' book_id=book_id %}" method="post" class="form-horizontal" role="form"> {% else %} <form action="{% url 'cms:book_add' %}" method="post" class="form-horizontal" role="form"> {% endif %} {% csrf_token %} {{ form|bootstrap_horizontal }} <div class="form-group"> <div class="col-sm-offset-2 col-sm-10"> <button type="submit" class="btn btn-primary">送信</button> </div> </div> </form> <a href="{% url 'cms:book_list' %}" class="btn btn-default btn-sm">戻る</a>{% endblock content %}

book_edit.html

Bootstrapの例

← django-bootstrap-form を使っているので   Form の項目を Bootstrap 形式で展開してくれる

Page 42: Djangoによるスマホアプリバックエンドの実装

{{ form|bootstrap_horizontal }}

form を丸ごと出す

django-bootstrap-formのテクニック

form を項目単位にバラす(項目を出す/出さない の制御をしたい時){{ form.id|bootstrap_horizontal }}{{ form.name|bootstrap_horizontal }}

HTMLレベルにバラす(checkbox、radioは微妙に異なるので注意)<div class="form-group{% if form.name.errors %} has-error{% endif %}"> <label class="control-label" for="{{ form.name.auto_id }}">{{ form.name.label }}</label> <input type="text" class=“form-control" name="{{ form.name.html_name }}" value="{{ form.name.value }}" id="{{ form.name.auto_id }}"> {% for error in form.name.errors %} <span class=“help-block {{ form.error_css_class }}">{{ error }}</span> {% endfor %} {% if form.name.help_text %} <p class="help-block"> {{ form.name.help_text|safe }} </p> {% endif %}</div>

checkbox、radioは bootstrapform/templates/bootstrapfrom/field.html でやっていることを真似ること

Page 43: Djangoによるスマホアプリバックエンドの実装

CRUDの書き方scaffold はないので、手で書くが、それほど大変ではない。

def book_list(request): '''書籍の一覧''' books = Book.objects.all().order_by('id') return render_to_response('cms/book_list.html', # 使用するテンプレート {'books': books}, # テンプレートに渡すデータ context_instance=RequestContext(request))

views.py 一覧

Page 44: Djangoによるスマホアプリバックエンドの実装

CRUDの書き方

def book_edit(request, book_id=None): '''書籍の編集''' if book_id: # book_id が指定されている (修正時) book = get_object_or_404(Book, pk=book_id) else: # book_id が指定されていない (追加時) book = Book() if request.method == 'POST': form = BookForm(request.POST, instance=book) # POST された request データからフォームを作成 if form.is_valid(): # フォームのバリデーション form.save() return redirect('cms:book_list') else: # GET の時 form = BookForm(instance=book) # book インスタンスからフォームを作成 return render_to_response('cms/book_edit.html', dict(form=form, book_id=book_id), context_instance=RequestContext(request))

views.py 登録/修正

forms.py class BookForm(ModelForm): '''書籍のフォーム''' class Meta: model = Book fields = ('name', 'publisher', 'page', )

Page 45: Djangoによるスマホアプリバックエンドの実装

CRUDの書き方

def book_del(request, book_id): '''書籍の削除''' book = get_object_or_404(Book, pk=book_id) book.delete() return redirect('cms:book_list')

views.py 削除

urls.py

urlpatterns = patterns('', # 書籍 url(r'^book/$', views.book_list, name='book_list'), # 一覧 url(r'^book/add/$', views.book_edit, name='book_add'), # 登録 url(r'^book/mod/(?P<book_id>\d+)/$', views.book_edit, name='book_mod'), # 修正 url(r'^book/del/(?P<book_id>\d+)/$', views.book_del, name='book_del'), # 削除)

Page 46: Djangoによるスマホアプリバックエンドの実装

• django.views.generic.list.ListView を使っておくと、ページネートが簡単

• Bootstrapのページネート部品とも相性がいい

Listページの書き方

class ImpressionList(ListView): '''感想の一覧''' context_object_name='impressions' template_name='cms/impression_list.html' paginate_by = 2 # 1ページは最大2件ずつでページングする! def get(self, request, *args, **kwargs): book = get_object_or_404(Book, pk=kwargs['book_id']) # 親の書籍を読む impressions = book.impressions.all().order_by('id') # 書籍の子供の、感想を読む self.object_list = impressions context = self.get_context_data(object_list=self.object_list, book=book) return self.render_to_response(context)

views.py 一覧

Page 47: Djangoによるスマホアプリバックエンドの実装

Listページの書き方

{% if is_paginated %} <ul class="pagination"> {% if page_obj.has_previous %} <li><a href="?page={{ page_obj.previous_page_number }}">&laquo;</a></li> {% else %} <li class="disabled"><a href="#">&laquo;</a></li> {% endif %} {% for linkpage in page_obj.paginator.page_range %} {% ifequal linkpage page_obj.number %} <li class="active"><a href="#">{{ linkpage }}</a></li> {% else %} <li><a href="?page={{ linkpage }}">{{ linkpage }}</a></li> {% endifequal %} {% endfor %} {% if page_obj.has_next %} <li><a href="?page={{ page_obj.next_page_number }}">&raquo;</a></li> {% else %} <li class="disabled"><a href="#">&raquo;</a></li> {% endif %} </ul> {% endif %}

impression_list.html のページング部分

Page 48: Djangoによるスマホアプリバックエンドの実装

Listページの書き方ページングの表示例

この部分

Page 49: Djangoによるスマホアプリバックエンドの実装

APIの実装

Page 50: Djangoによるスマホアプリバックエンドの実装

Django REST framework とかもあるけど・・          

Page 51: Djangoによるスマホアプリバックエンドの実装

レスポンスを自前でJSONで書く

Page 52: Djangoによるスマホアプリバックエンドの実装

辞書をsimplejson.dumps()で返した場合

def org_list(request): '''会社の一覧''' orgs = [] for org in Organization.objects.all(): orgs.append(dict(id=org.id, name=org.name)) data = dict(status='ok', response_code= '000', message='Success', org_list=orgs) json = simplejson.dumps(data, ensure_ascii=False, indent=2) return HttpResponse(json, mimetype='application/json; charset=UTF-8')

Page 53: Djangoによるスマホアプリバックエンドの実装

項目が順不同になる・・{ "org_list": [ { "id": 1, "name": "Japan System Laboratory" }, { "id": 2, "name": "GEEKLAB.NAGANO" } ], "status": "ok", "message": "Success", "response_code": "000" }

Page 54: Djangoによるスマホアプリバックエンドの実装

アプリ開発者から文句を言われるw・・

Page 55: Djangoによるスマホアプリバックエンドの実装

なので・・

Page 56: Djangoによるスマホアプリバックエンドの実装

順序付き辞書を使いましょう!(Python 2.7~)

OrderedDict()で順序付き辞書にする

from collections import OrderedDictdef org_list(request): ''' 会社の一覧を返す ''' orgs = [] for org in Organization.objects.all(): orgs.append(dict(id=org.id, name=org.name)) data = OrderedDict([('status', 'ok'), ('response_code', '000'), ('message', 'Success'), ('org_list',orgs)]) json = simplejson.dumps(data, ensure_ascii=False, indent=2) return HttpResponse(json, mimetype='application/json; charset=UTF-8')

Page 57: Djangoによるスマホアプリバックエンドの実装

ちゃんとコード通りSortされる{ "status": "ok", "response_code": "000", "message": "Success", "org_list": [ { "name": "Japan System Laboratory", "id": 1 }, { "name": "GEEKLAB.NAGANO", "id": 2 } ] }

Page 58: Djangoによるスマホアプリバックエンドの実装

• 簡単なデータの場合は、CMSのForm受信と同じものを書いて、スマホ側では http form post を模倣してもらう。

◦ こうすることによって、データのエラーチェックは、 フォームのバリデーションの仕組みが使えます。

◦ 正常かエラーかは、JSONで結果を返すようにしま す。

!

• 繰り返しがある複雑なデータは、スマホ側からJSONをPOSTしてもらい、json.loads() でデコードする

Postの場合

Page 59: Djangoによるスマホアプリバックエンドの実装

#ログインフォームclass MemberLoginForm(forms.Form): email = forms.CharField(label='email', max_length=255) password = forms.CharField(label='password', max_length=255)

簡単なデータの場合formの作成

@csrf_exemptdef user_login(request): if request.method == 'POST': form = MemberLoginForm(request.POST) if not form.is_valid(): email = form.cleaned_data['email'] password = form.cleaned_data['password'] data = OrderedDict([ ('status', 'ng'), ('response_code', '001'), ('message', form.errors) ]) return render_json_response(request, data) return render_json_response(request, data) else: form = MemberLoginForm() return render_to_response('api/user_login.html', dict(form=form), context_instance=RequestContext(request))

ファンクションの作成

Page 60: Djangoによるスマホアプリバックエンドの実装

簡単なデータの場合

Page 61: Djangoによるスマホアプリバックエンドの実装

JSON_QUIZ_RESPONSE = '''{ "quiz_questions":[ { "quiz_question_id":2, "checked_quiz_options": [ {"quiz_option_id":6} ] } ]}'''# 4択クイズ回答フォームclass ModuleQuizResponseForm(forms.Form): user_id = forms.IntegerField(label='user_id') # ユーザID json_string = forms.CharField(label='json_string', widget=forms.Textarea, initial=JSON_QUIZ_RESPONSE)

複雑なデータの場合formの作成(JSONのテンプレをplaceholderで表示してあげると親切)

Page 62: Djangoによるスマホアプリバックエンドの実装

複雑なデータの場合

この部分

Page 63: Djangoによるスマホアプリバックエンドの実装

複雑なデータの場合

@csrf_exemptdef module_quiz_response(request, module_id): if request.method == 'POST': form = ModuleQuizResponseForm(request.POST) if form.is_valid(): '''省略'''! # JSON文字列の取り出し json_string = form.cleaned_data['json_string'] json_obj = json.loads(json_string) analyze_quiz_questions = [] # クイズ回答ログ、初回回答の更新 for json_question in json_obj['quiz_questions']: quiz_question_id = json_question['quiz_question_id'] '''省略''' data = OrderedDict([ ('status', 'ok'), ('response_code', '000'), ('message', 'Success'), ]) return render_json_response(request, data) else: form = ModuleQuizResponseForm() return render_to_response('api/module_quiz_response.html', dict(form=form, module_id=module_id), context_instance=RequestContext(request))

ファンクションの作成(json.loadsでJSONを解析)

Page 64: Djangoによるスマホアプリバックエンドの実装

PUSH通知

Page 65: Djangoによるスマホアプリバックエンドの実装

to iOS

Page 66: Djangoによるスマホアプリバックエンドの実装

APNs (Apple Notification

Service)

Page 67: Djangoによるスマホアプリバックエンドの実装

• 最新バージョン 1.1.2(今のところPython3は未対応)https://github.com/djacobs/PyAPNs

• 事前に証明書ファイル・キーファイルを作成しておく

PyAPNs

$ pip install git+git://github.com/djacobs/PyAPNs.git

※普通に入れると、期待したものが入らない可能性があるので、 GitHubからインストール

Page 68: Djangoによるスマホアプリバックエンドの実装

1デバイスへのPUSH通知

from apns import APNs, Frame, Payloaddef send_push_message(token_hex): apns = APNs(use_sandbox=True, cert_file='xxx.pem', key_file='xxx_key-noenc.pem')  payload = Payload(alert="Hello World!", sound="default", badge=1) # Send a notification apns.gateway_server.send_notification(token_hex, payload)

Page 69: Djangoによるスマホアプリバックエンドの実装

複数デバイスへのPUSH通知 最新の形式(frame形式?)

from apns import APNs, Payloaddef send_push_message(token_hex): apns = APNs(use_sandbox=False, cert_file='xxx.pem', key_file='xxx-noenc.pem')! # 対象のデバイスのトークンをまとめる tokens = ['xxxxxxxxxxxxxxxxxx','xxxxxxxxxxxxxxxxxx']! payload = Payload(alert="Hello World!", sound="default", badge=1) # 一括でPUSH frame = Frame() identifier = 1 expiry = time.time()+3600 priority = 10 # 即座に通知 for token in tokens: frame.add_item(token, payload, identifier, expiry, priority) apns.gateway_server.send_notification_multiple(frame)

Page 70: Djangoによるスマホアプリバックエンドの実装

feedbackで返された トークンは、削除する

for (token_hex, fail_time) in apns.feedback_server.items(): #未使用のデバイストークンを削除 for token in DeviceToken.objects.filter(token=token_hex): token.delete()!

Page 71: Djangoによるスマホアプリバックエンドの実装

• デバイストークンを収集する仕組み > API    *ユーザの複数端末持ちを考慮

• ペイロードのサイズ制限は256バイト > 冗長したメッセージは「・・・」等で調整

実装上のポイント

Page 72: Djangoによるスマホアプリバックエンドの実装

to Android

Page 73: Djangoによるスマホアプリバックエンドの実装

GCM (Google Cloud Message)

Page 74: Djangoによるスマホアプリバックエンドの実装

• 最新バージョンは 0.1.5https://github.com/geeknam/python-gcm

• APIキーを事前にGoogle API Consoleから取得

python-gcm

$ pip install python-gcm

Page 75: Djangoによるスマホアプリバックエンドの実装

# APIキーを渡して、GCMオブジェクトを作成gcm = GCM('XXXXXXXXXXXXXXXXXXXXXXXXX')!# registration idを指定するreg_ids = ['XXXXXXXXXXXXX','XXXXXXXXXXXXX','XXXXXXXXXXXXX'] data = {'alert': 'テスト!!' }!# PUSH response = gcm.json_request(registration_ids=reg_ids, data=data) if 'canonical' in response: #GCMサーバーがcanonical idを返したきた場合、現状のデバイストークン(register id)をこちらに置き換える for canonical_id, reg_id in response['canonical'].items(): for token in DeviceToken.objects.filter(device_token=reg_id): token.device_token = canonical_id token.save()

python-gcmの使用例

Page 76: Djangoによるスマホアプリバックエンドの実装

• デバイストークンを収集する仕組み > API    *ユーザの複数端末持ちを考慮(APNsと同様)

• ペイロードのサイズ制限は4096バイト > 気にしなくて良いレベル

実装上のポイント

Page 77: Djangoによるスマホアプリバックエンドの実装

• サーバ/スマホ間のパスワード通信を暗号化したい

• iOS/Android/Python で共通で暗号化/復号化できるベストなプロトコルは何か

• AESがよい(AES ECBモード)AESの暗号化はバイナリ値になるのでBASE64に変換

• pycrypto を使うhttps://www.dlitz.net/software/pycrypto/

ログイン パスワードの暗号化

$ pip install pycrypto

Page 78: Djangoによるスマホアプリバックエンドの実装

ログイン パスワードの暗号化

AES 復号化の部分

from Crypto.Cipher import AESfrom Crypto import Random!def aes_decrypt(string, key=None): ''' AESで復号化 ''' if not key or len(key) not in (16, 128, 192, 256): raise ValueError('Key size must be 16, 128, 192, 256') bs = AES.block_size iv = Random.new().read(bs) cipher = AES.new(key.encode(), AES.MODE_ECB, iv)! plaintext = cipher.decrypt(string) return plaintext.decode().rstrip('\0')

Page 79: Djangoによるスマホアプリバックエンドの実装

ログイン パスワードの暗号化 BASE64のデコードimport base64!def base64url_decode(input): ''' BASE64のデコード ''' rem = len(input) % 4 if rem > 0: input += '=' * (4 - rem) try: return base64.urlsafe_b64decode(input.encode()).decode() # return str except UnicodeDecodeError: return base64.urlsafe_b64decode(input.encode()) # return byte

ログイン処理のパスワード復号化

AES_KEY = getattr(settings, 'AES_KEY', 'SomeAesKey16byte') password_decrypt = aes_decrypt(base64url_decode(password), AES_KEY)

Page 80: Djangoによるスマホアプリバックエンドの実装

• Twitter/Facebook などの OAuth 2.0 連携は、python-social-authで用意されている

- https://github.com/omab/python-social-auth

• OpenID Connect でログイン連携したい

- 今後多くなると思われ

• Yahoo Janan! の OpenID Connect (YConnect) の胸を借りる

- 公開してくれている Yahoo Janan! に感謝を!

ログイン連携

Page 81: Djangoによるスマホアプリバックエンドの実装

• python-social-auth の拡張モジュールを書く

- 本家でもOpenID Connectは未対応?

- 自分で書くことにした ‣ ベースはOAuth2.0で行ける ‣ OpenIDっぽいnonceの処理がある ‣ JWT (JSON Web Token)のデコードを追加

• サンプルはGitHub Gistを参照(長いので割愛)

ログイン連携

https://gist.github.com/kakky/6809432

Page 82: Djangoによるスマホアプリバックエンドの実装

アプリケーションを公開する

Page 83: Djangoによるスマホアプリバックエンドの実装

AWSで公開する

Mobile Client

iOS/Android アプリ Amazon EC2

Amazon S3

画像、音声、映像

Amazon RDS

DBサーバー (MySQL)

Amazon SESEmail

AWS SDK for Python (boto)

普通に SMTPサーバー として指定

IPアドレス指定

量が少ない場合は GMail、Google Apps で済ませてしまう場合もあり

Page 84: Djangoによるスマホアプリバックエンドの実装

• URLに変換する際に、bucketはホスト名の一部になるため、全世界で一意にする

• keyの部分は、/を使って任意にフォルダ的なものを作ることができる

• bucket=my-bucket-name、key=path/to/image.jpg とすると、 以下のようなURLを生成できる

S3 の bucket と key の関係

$ pip install boto

画像ファイル等をS3に追い出すために、まずはこれ

botoによるAmazon S3連携

https://my-bucket-name.s3.amazonaws.com/path/to/image.jpg

Page 85: Djangoによるスマホアプリバックエンドの実装

botoによるAmazon S3連携 S3へのアップロードと、パブリックなURLの取得import boto, mimetypes, osfrom boto.s3.key import Key!def s3_upload_media(file_path, s3_bucket, s3_key, do_delete=True): '''S3へのアップロードと、URLの取得''' conn = boto.connect_s3() b = conn.get_bucket(s3_bucket) k = Key(b) k.key = s3_key k.set_metadata("Content-Type", mimetypes.guess_type(k.key)[0]) k.set_contents_from_filename(file_path) # アップロード k.set_acl('public-read') # アクセス権を設定し、URLで見れるようにする s3_url = k.generate_url(3600, query_auth=False) #バケットとキーからURLを生成 if do_delete: os.remove(file_path) # 元ファイルの削除 return s3_url # DBには、このURL(と削除のためにs3_key)を格納する

※S3へのaccess_key、secret_access_keyなどのCredentialは、~/.boto に置いてあると仮定

Page 86: Djangoによるスマホアプリバックエンドの実装

uWSGI vassal

uWSGI vassal

nginx

• Djangoアプリケーションのデプロイは以下を使用

- nginx : Webサーバ

- uWSGI : アプリケーション コンテナ サーバ ‣ 姉妹サービスを同一ホストで公開することも踏まえ ‣ emperor/vassals(皇帝/家臣)モードを使用

nginx + uWSGI

皇帝

家臣/家来? サービス1 仮想ホスト1

サービス2 仮想ホスト2

upstream 起動

uWSGI emperor

Page 87: Djangoによるスマホアプリバックエンドの実装

nginxの設定 upstream django-myservice { server unix:/tmp/uwsgi-myservice.sock;}server { listen 80; server_name www.myservice.com; uwsgi_buffer_size 4k; uwsgi_buffers 32 4k; : location /static/admin { alias /usr/lib/python2.7/site-packages/django/contrib/admin/static/admin; } location /static { alias /var/www/django/myservice/static; } location /media { alias /var/www/django/myservice/media; } location / { include uwsgi_params; uwsgi_pass django-myservice; }}

Page 88: Djangoによるスマホアプリバックエンドの実装

uWSGI emperorの設定

# /etc/uwsgi.yamluwsgi: emperor: /etc/uwsgi/vassals uid: nginx gid: nginx logfile-chmod: 644 daemonize: /var/log/uwsgi/emperor.log touch-logreopen: /tmp/uwsgi-log-reopen.txt

emperor側は、/etc/uwsgi/vassels/ の下にある vasselsの設定ファイルを起動せよ、と書いてあるだけ

この /etc/uwsgi.yaml は、 /etc/rc.d/init.d/uwsgi にスクリプトを書いて   $ sudo service uwsgi startにて起動できるようにしているが、長いので割愛(すみません)

Page 89: Djangoによるスマホアプリバックエンドの実装

uWSGI emperorの設定

#!/bin/sh## /etc/rc.d/init.d/uwsgi## uwsgi - this script starts and stops the uwsgi daemon## chkconfig: - 85 15# processname: uwsgi# config: /etc/uwsgi.yaml# config: /etc/sysconfig/uwsgi# pidfile: /var/run/uwsgi.pid# description: uwsgi is a WSGI server#!# Source function library.. /etc/rc.d/init.d/functions!CONFFILE="/etc/uwsgi.yaml"!if [ -f /etc/sysconfig/uwsgi ]; then . /etc/sysconfig/uwsgifi!prog=uwsgiuwsgi=${NGINX-/usr/bin/uwsgi}conffile=${CONFFILE-/etc/uwsgi.yaml}lockfile=${LOCKFILE-/var/lock/subsys/uwsgi}pidfile=${PIDFILE-/var/run/uwsgi.pid}RETVAL=0!start() { echo -n $"Starting $prog: "! #daemon --pidfile=${pidfile} ${uwsgi} --yaml ${conffile} daemon ${uwsgi} --yaml ${conffile} --pidfile ${pidfile} RETVAL=$? echo [ $RETVAL = 0 ] && touch ${lockfile} return $RETVAL}!stop() { echo -n $"Stopping $prog: " killproc -p ${pidfile} ${prog} -INT RETVAL=$? echo [ $RETVAL = 0 ] && rm -f ${lockfile} ${pidfile}}!# See how we were called.case "$1" in start) start ;; stop) stop ;; status) status -p ${pidfile} ${uwsgi} RETVAL=$? ;; restart) stop start ;; *) echo $"Usage: $prog {start|stop|restart|status}" RETVAL=2esac!exit $RETVAL

とはいえ、後からスライドを見て、コピペしたい人用に /etc/rc.d/init.d/uwsgi のスクリプトを貼っておきます

今、見えなくても怒らないで (́・ω・`)

Page 90: Djangoによるスマホアプリバックエンドの実装

uWSGI vassalsの設定 # /etc/uwsgi/vassals/myservice_uwsgi.yamluwsgi: socket: /tmp/uwsgi-myservice.sock chmod-socket: 666 chdir: /var/www/django/myservice/ wsgi-file: myservice/uwsgi.py master: true enable-threads: true pidfile: /tmp/uwsgi-myservice-master.pid processes: 2 threads: 30 stats: 127.0.0.1:9191 no-orphans: true touch-reload: /tmp/uwsgi-myservice-reload.txt uid: nginx gid: nginx vacuum: true import: uwsgi_autoreload logfile-chmod: 644 log-date: [%%a %%b %%d %%H:%%M:%%S %%Y] daemonize: /var/log/uwsgi/myservice.log disable-logging: true touch-logreopen: /tmp/uwsgi-log-reopen.txt listen: 4096

正直、パラメータ大杉 !性能が出る/出ない エラー吐く/吐かない はパラメータ次第

Apache+mod_wsgi の方が、よろしくやってくれた感がある

Page 91: Djangoによるスマホアプリバックエンドの実装

• 開発サーバと同じく、コードをデプロイしたら、自動的に再起動して反映してほしい

• 果たして、プロダクションでそれをやっていいかは議論の余地があるが、便利なので設定する

uWSGIでオートリロード

Page 92: Djangoによるスマホアプリバックエンドの実装

uWSGIでオートリロード

# -*- coding: utf-8 -*-# nginx + uWSGI で実行した時、ソースコードの変更を検知して、uWSGIを再起動する## 注) import uwsgi は uWSGI 配下で実行した時のみ参照できる# from uwsgidecorators も同様# どちらもローカル開発時は Unresolved import のままでよい!import uwsgifrom uwsgidecorators import timerfrom django.utils import autoreload!@timer(3) # 3秒ごとに呼ばれるdef change_code_gracefull_reload(sig): if autoreload.code_changed(): print(‘code change detected. autoreload ——————————————————————‘) uwsgi.reload()

プロジェクトのディレクトリ直下に、uwsgi_autoreload.py というコードを置く

Page 93: Djangoによるスマホアプリバックエンドの実装

uWSGIでオートリロード

# /etc/uwsgi/vassals/myservice_uwsgi.yamluwsgi: : : import: uwsgi_autoreload : :

uWSGI vassals の設定ファイルで指定する

Page 94: Djangoによるスマホアプリバックエンドの実装

Qiitaにチュートリアル書きました

http://qiita.com/kaki_k/items/511611cadac1d0c69c54

Page 95: Djangoによるスマホアプリバックエンドの実装

Qiitaにチュートリアル書きました

• 「Django入門」でググると、一番上に出てきてビビリます

• Djangoを使う人の裾野を広げたいと思い書きました。

• 公式チュートリアルと合わせて、新しい人材の育成にご活用下さい。

Page 96: Djangoによるスマホアプリバックエンドの実装

スマートフォンとの連携案件を、Djangoを使ってどんどん作りましょう!

まとめ

• コード部分は小さい字が多くてすみません。

• スライドは後ほど公開しますので、小さくて見えなかった部分は、後で見返して下さい。

• ということで、

Page 97: Djangoによるスマホアプリバックエンドの実装

ご清聴ありがとうございました