Djangoによるスマホアプリバックエンドの実装
-
Upload
nakazawa-yuichi -
Category
Software
-
view
2.293 -
download
4
description
Transcript of Djangoによるスマホアプリバックエンドの実装
Djangoによる スマホアプリ バックエンドの実装
PyCon JP 2014
Yuichi Nakazawa
@y_nakazawa1220
(株)日本システム技研 所属
スマホアプリのバックエンド開発
GEEKLAB.NAGANO 管理人
自己紹介
Kazuhiko Kakita
@kaki_k
(株)日本システム技研 所属
スマホアプリのバックエンド開発
GEEKLAB.NAGANO 管理人
自己紹介
キャスタリア株式会社【goocus pro】
”モバイル&ソーシャル”をコンセプトに設計された、”Mobile Native”なラーニングプラットフォーム『goocus pro』
『B2B』SaaS型のサービスとして、企業様・学校様等にご提供
教育ビジネスを展開される企業様にプラットフォームとしてご提供
『B2B2B』『B2B2C』
プログラミング学習が必修の通信制高等学校「コードアカデミー高等学校」を設立しました
『「ソーシャルラーニング」入門 ソーシャルメディアがもたらす人と組織の知識革命』の翻訳を手掛けました
国内外の先端的な教育/学習の最新情報をお届けするブログを運営しています
日本オープンオンライン教育推進協議会『JMOOC』に正会員として参加していますhttp://www.castalia.co.jp
GEEKLAB. NAGANO
http://geeklab-nagano.com
GEEKLAB. NAGANO
GEEKLAB. NAGANOとは
• 地元のエンジニアを集めて勉強会・セミナーを開催
• 知識・ノウハウの集積基地
• 長野からのITの発信を!!
設備(全部無料です!)
GEEKLAB. NAGANO
椅子
テーブル ソファー
単焦点プロジェクター
非破壊スキャナ
ホワイトボード
IT書籍、雑誌
インターネット接続(WiFi, 有線)
Apple TV
電子工作機器
自販機
スライム・・
GEEKLAB. NAGANO
• 利用可能日時:平日は9-18時頃(勉強会・セミナー以外)。土日祝日は問い合わせ要
• 運営: 学校法人 信学会株式会社日本システム技研(JSL)キャスタリア株式会社
利用時間・運営
長野に来られた際は、是非お立ち寄りを!!
• モバイルファースト!
• スマートフォンと連携したバックエンド開発が沢山出てくる時代
はじめに
Djangoでバックエンドを作ろう!
• 学習コストが低い
• フルスタックのフレームワーク
• scaffoldは無いけど、管理サイトが秀逸
Djangoのメリット
管理サイト ログイン画面
管理サイト Model選択
管理サイト 一覧画面
管理サイト 追加画面
• 管理サイト + API < 最低限これだけあればOK
• 管理サイト + CMS + API < ここまであれば完璧
アプリケーションの形態
CMS部分を作る
• 基本となる親子関係のモデルを作る
モデル定義
たとえば、このようなモデル
書籍
感想
1:多
# -*- 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が作ってくれる
• 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)
ORMのリレーションで出来ること
1.多対一のリレーション ForeignKey
Manufacturer
Car
1:多
2.多対多のリレーション ManyToManyField
Topping Pizza
多:多
再帰的リレーション (自分自身に対する多対一のリレーション) も可
再帰的リレーション (自分自身に対する多対多のリレーション) も可
(中間モデル)
中間モデルは、DB上に隠しテーブルができるが、 意識しなくてよい
3.エクストラフィールドで多対多のリレーション ManyToManyField の through 引数
Person Group
多:多
Membership
中間モデルに項目を持たせて、自分で定義したい場合
4.一対一のリレーション OneToOneField
Restaurant
Place
1:1
モデルを継承して項目追加する代わりに OneToOneField で項目追加したモデルを作る !継承ができないかというと、そうではない
ORMのリレーションで出来ること
モデルの継承
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): ...
一般化リレーション
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) は不要
• 様々なリレーションの作り方をご紹介しました。
• これらを駆使してモデル図を設計すれば、作りたいデータベースのモデル定義ができると思います。
ORMまとめ
• アグリゲーション(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
• Django 1.6まではSouth - http://south.aeracode.org
• Django 1.7からは標準として取り込まれた
• モデル変更が楽
• model.py の定義変更をDBに反映させることができる
• modelを直すとmigrateファイルを作ってくれる(某フレームワークとは逆)
DB migration
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
DB migration
class Book(models.Model): : page = models.IntegerField(u'ページ数', blank=True, default=0)
新たなアプリケーションを作って、models.pyを書いた初回
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に作成される
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) # 追加
ここから日常の作業として、
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のテーブルに反映される
• CSSフレームワーク http://getbootstrap.com/ • エンジニアだけで作っても見栄えを良くする
Bootstrapを使う
• Djangoのテンプレートは継承できるので、以下のように
Bootstrap
BootstrapのJS、CSS を定義したベース
Navbar ヘッダーのナビバー
Navbarを使わないもの ログイン など
Navbarを使うもの CMSの各種ページ
base.html
base_navi.html
index.html などlogin.html など
• 一覧系のページは、Bootstrapのclassを使って普通に書く
• フォーム系のページは、django-bootstrap-form https://github.com/tzangms/django-bootstrap-form を使う
Bootstrap
使い方としては
$ pip install django-bootstrap-form
{% 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を記述する ベースとなるテンプレート
{% 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 ブロックを置き換え
{% 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 ブロックを置き換え
{% 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 形式で展開してくれる
{{ 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 でやっていることを真似ること
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 一覧
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', )
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'), # 削除)
• 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 一覧
Listページの書き方
{% if is_paginated %} <ul class="pagination"> {% if page_obj.has_previous %} <li><a href="?page={{ page_obj.previous_page_number }}">«</a></li> {% else %} <li class="disabled"><a href="#">«</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 }}">»</a></li> {% else %} <li class="disabled"><a href="#">»</a></li> {% endif %} </ul> {% endif %}
impression_list.html のページング部分
Listページの書き方ページングの表示例
この部分
APIの実装
Django REST framework とかもあるけど・・
レスポンスを自前でJSONで書く
辞書を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')
項目が順不同になる・・{ "org_list": [ { "id": 1, "name": "Japan System Laboratory" }, { "id": 2, "name": "GEEKLAB.NAGANO" } ], "status": "ok", "message": "Success", "response_code": "000" }
アプリ開発者から文句を言われるw・・
なので・・
順序付き辞書を使いましょう!(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')
ちゃんとコード通りSortされる{ "status": "ok", "response_code": "000", "message": "Success", "org_list": [ { "name": "Japan System Laboratory", "id": 1 }, { "name": "GEEKLAB.NAGANO", "id": 2 } ] }
• 簡単なデータの場合は、CMSのForm受信と同じものを書いて、スマホ側では http form post を模倣してもらう。
◦ こうすることによって、データのエラーチェックは、 フォームのバリデーションの仕組みが使えます。
◦ 正常かエラーかは、JSONで結果を返すようにしま す。
!
• 繰り返しがある複雑なデータは、スマホ側からJSONをPOSTしてもらい、json.loads() でデコードする
Postの場合
#ログインフォーム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))
ファンクションの作成
簡単なデータの場合
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で表示してあげると親切)
複雑なデータの場合
この部分
複雑なデータの場合
@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を解析)
PUSH通知
to iOS
APNs (Apple Notification
Service)
• 最新バージョン 1.1.2(今のところPython3は未対応)https://github.com/djacobs/PyAPNs
• 事前に証明書ファイル・キーファイルを作成しておく
PyAPNs
$ pip install git+git://github.com/djacobs/PyAPNs.git
※普通に入れると、期待したものが入らない可能性があるので、 GitHubからインストール
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)
複数デバイスへの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)
feedbackで返された トークンは、削除する
for (token_hex, fail_time) in apns.feedback_server.items(): #未使用のデバイストークンを削除 for token in DeviceToken.objects.filter(token=token_hex): token.delete()!
• デバイストークンを収集する仕組み > API *ユーザの複数端末持ちを考慮
• ペイロードのサイズ制限は256バイト > 冗長したメッセージは「・・・」等で調整
実装上のポイント
to Android
GCM (Google Cloud Message)
• 最新バージョンは 0.1.5https://github.com/geeknam/python-gcm
• APIキーを事前にGoogle API Consoleから取得
python-gcm
$ pip install python-gcm
# 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の使用例
• デバイストークンを収集する仕組み > API *ユーザの複数端末持ちを考慮(APNsと同様)
• ペイロードのサイズ制限は4096バイト > 気にしなくて良いレベル
実装上のポイント
• サーバ/スマホ間のパスワード通信を暗号化したい
• iOS/Android/Python で共通で暗号化/復号化できるベストなプロトコルは何か
• AESがよい(AES ECBモード)AESの暗号化はバイナリ値になるのでBASE64に変換
• pycrypto を使うhttps://www.dlitz.net/software/pycrypto/
ログイン パスワードの暗号化
$ pip install pycrypto
ログイン パスワードの暗号化
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')
ログイン パスワードの暗号化 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)
• Twitter/Facebook などの OAuth 2.0 連携は、python-social-authで用意されている
- https://github.com/omab/python-social-auth
• OpenID Connect でログイン連携したい
- 今後多くなると思われ
• Yahoo Janan! の OpenID Connect (YConnect) の胸を借りる
- 公開してくれている Yahoo Janan! に感謝を!
ログイン連携
• python-social-auth の拡張モジュールを書く
- 本家でもOpenID Connectは未対応?
- 自分で書くことにした ‣ ベースはOAuth2.0で行ける ‣ OpenIDっぽいnonceの処理がある ‣ JWT (JSON Web Token)のデコードを追加
• サンプルはGitHub Gistを参照(長いので割愛)
ログイン連携
https://gist.github.com/kakky/6809432
アプリケーションを公開する
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 で済ませてしまう場合もあり
• 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
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 に置いてあると仮定
uWSGI vassal
uWSGI vassal
nginx
• Djangoアプリケーションのデプロイは以下を使用
- nginx : Webサーバ
- uWSGI : アプリケーション コンテナ サーバ ‣ 姉妹サービスを同一ホストで公開することも踏まえ ‣ emperor/vassals(皇帝/家臣)モードを使用
nginx + uWSGI
皇帝
家臣/家来? サービス1 仮想ホスト1
サービス2 仮想ホスト2
upstream 起動
uWSGI emperor
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; }}
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にて起動できるようにしているが、長いので割愛(すみません)
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 のスクリプトを貼っておきます
今、見えなくても怒らないで (́・ω・`)
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 の方が、よろしくやってくれた感がある
• 開発サーバと同じく、コードをデプロイしたら、自動的に再起動して反映してほしい
• 果たして、プロダクションでそれをやっていいかは議論の余地があるが、便利なので設定する
uWSGIでオートリロード
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 というコードを置く
uWSGIでオートリロード
# /etc/uwsgi/vassals/myservice_uwsgi.yamluwsgi: : : import: uwsgi_autoreload : :
uWSGI vassals の設定ファイルで指定する
Qiitaにチュートリアル書きました
http://qiita.com/kaki_k/items/511611cadac1d0c69c54
Qiitaにチュートリアル書きました
• 「Django入門」でググると、一番上に出てきてビビリます
• Djangoを使う人の裾野を広げたいと思い書きました。
• 公式チュートリアルと合わせて、新しい人材の育成にご活用下さい。
スマートフォンとの連携案件を、Djangoを使ってどんどん作りましょう!
まとめ
• コード部分は小さい字が多くてすみません。
• スライドは後ほど公開しますので、小さくて見えなかった部分は、後で見返して下さい。
• ということで、
ご清聴ありがとうございました