Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

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

16,781 views

Published on

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

Published in: Software
  • Be the first to comment

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

  1. 1. Djangoによる スマホアプリ バックエンドの実装 PyCon JP 2014
  2. 2. 自己紹介 Yuichi Nakazawa @y_nakazawa1220 (株)日本システム技研 所属 スマホアプリのバックエンド開発   GEEKLAB.NAGANO 管理人
  3. 3. 自己紹介 Kazuhiko Kakita @kaki_k (株)日本システム技研 所属 スマホアプリのバックエンド開発   GEEKLAB.NAGANO 管理人
  4. 4. 株式会社 日本システム技研 http://jsl.co.jp
  5. 5. キャスタリア株式会社 【goocus pro】 ”モバイル&ソーシャル”をコンセプトに設計された、 ”Mobile Native”なラーニングプラットフォーム『goocus pro』 『B2B』 SaaS型のサービスとして、 企業様・学校様等にご提供 『B2B2B』『B2B2C』 教育ビジネスを展開される企業 様にプラットフォームとしてご 提供 プログラミング学習が必 修の通信制高等学校「コー ドアカデミー高等学校」 を設立しました 『「ソーシャルラーニン グ」入門 ソーシャルメ ディアがもたらす人と組 織の知識革命』の翻訳を 手掛けました 国内外の先端的な教育/学習の最新情 報をお届けするブログを運営していま す 日本オープンオンライン教育推進 協議会『JMOOC』に正会員として http://www.castalia.co.jp 参加しています
  6. 6. GEEKLAB. NAGANO http://geeklab-nagano.com
  7. 7. GEEKLAB. NAGANO GEEKLAB. NAGANOとは • 地元のエンジニアを集めて 勉強会・セミナーを開催 • 知識・ノウハウの集積基地 • 長野からのITの発信を!!
  8. 8. GEEKLAB. NAGANO 設備(全部無料です!) 椅子 テーブルソファー 単焦点プロジェクター 非破壊スキャナ IT書籍、雑誌 ホワイトボード インターネット接続(WiFi, 有線) Apple TV 電子工作機器 自販機 スライム・・
  9. 9. GEEKLAB. NAGANO 利用時間・運営 • 利用可能日時:平日は9-18時頃(勉強会・セミナー以 外)。土日祝日は問い合わせ要 • 運営:                          学校法人 信学会 株式会社日本システム技研(JSL) キャスタリア株式会社
  10. 10. 長野に来られた際は、 是非お立ち寄りを!!
  11. 11. はじめに • モバイルファースト! • スマートフォンと連携した バックエンド開発が沢山出てくる時代
  12. 12. Djangoでバックエンドを作ろう!
  13. 13. Djangoのメリット • 学習コストが低い • フルスタックのフレームワーク • scaffoldは無いけど、管理サイトが秀逸
  14. 14. 管理サイト ログイン画面
  15. 15. 管理サイト Model選択
  16. 16. 管理サイト 一覧画面
  17. 17. 管理サイト 追加画面
  18. 18. アプリケーションの形態 • 管理サイト + API < 最低限これだけあればOK • 管理サイト + CMS + API < ここまであれば完璧
  19. 19. CMS部分を作る
  20. 20. モデル定義 • 基本となる親子関係のモデルを作る たとえば、このようなモデル 書籍 1:多 感想
  21. 21. models.py の例 # -*- 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.ForeignKeyがみそ • これがDBの定義となり、CREATE TABLE文はDjangoが作ってくれる
  22. 22. ORM (Object Relation Mapping) • 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') # 書籍の子供の、感想を読む : : !
  23. 23. ORMのリレーションで出来ること 1.多対一のリレーション ForeignKey Manufacturer Car 1:多 2.多対多のリレーション ManyToManyField Topping Pizza 多:多 再帰的リレーション (自分自身に対する多対一のリレーション) も可 (中間モデル) 中間モデルは、DB上に隠しテーブルができるが、 意識しなくてよい 再帰的リレーション (自分自身に対する多対多のリレーション) も可
  24. 24. ORMのリレーションで出来ること 3.エクストラフィールドで多対多のリレーション ManyToManyField の through 引数 Person Group 多:多 Membership 中間モデルに項目を持たせて、自分で定義したい場合 4.一対一のリレーション OneToOneField Place 1:1 Restaurant モデルを継承して項目追加する代わりに OneToOneField で項目追加したモデルを作る ! 継承ができないかというと、そうではない
  25. 25. モデルの継承 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): ...
  26. 26. 一般化リレーション 1.一般化リレーション 色々な親モデルにタグを付けたい場合など User TaggedItem class TaggedItem(models.Model): tag = models.SlugField() content_type = models.ForeignKey(ContentType) object_id = models.PositiveIntegerField() content_object = generic.GenericForeignKey('content_type', 'object_id') 逆参照しなければ tags = generic.GenericRelation(TaggedItem) は不要 Bookmark class Bookmark(models.Model): url = models.URLField() tags = generic.GenericRelation(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
  27. 27. ORMまとめ • 様々なリレーションの作り方をご紹介しました。 • これらを駆使してモデル図を設計すれば、作りたい データベースのモデル定義ができると思います。
  28. 28. アグリゲーションを使いこなす • アグリゲーション(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_books 73
  29. 29. DB migration • Django 1.6まではSouth - http://south.aeracode.org • Django 1.7からは標準として取り込まれた • モデル変更が楽 • model.py の定義変更をDBに反映させることができる • modelを直すとmigrateファイルを作ってくれる (某フレームワークとは逆)
  30. 30. 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 # 作りたい場合は、任意で実行
  31. 31. DB migration 新たなアプリケーションを作って、models.pyを書いた初回 class Book(models.Model): : page = models.IntegerField(u'ページ数', blank=True, default=0)
  32. 32. DB migration makemigrationsコマンド(models.pyの変更を拾う) $ python manage.py makemigrations myapp makemigrationsが作成したマイグレーション ファイルを確認 myproj/myapp/migrations/0001_initial.py などといったファイルができているので、エディタで確認する migrateコマンドで、変更をDBに反映する $ python manage.py migrate myapp 新たなモデルがテーブルとしてDBに作成される
  33. 33. 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) # 追加
  34. 34. DB migration makemigrationsコマンド(models.pyの変更を拾う) $ python manage.py makemigrations myapp makemigrationsが作成したマイグレーション ファイルを確認 myproj/myapp/migrations/0002_book_isbn.py などといったファイルができているので、エディタで確認する migrateコマンドで、変更をDBに反映する $ python manage.py migrate myapp モデルの項目追加/変更がDBのテーブルに反映される
  35. 35. Bootstrapを使う • CSSフレームワーク http://getbootstrap.com/ • エンジニアだけで作っても見栄えを良くする
  36. 36. Bootstrap • Djangoのテンプレートは継承できるので、以下のように BootstrapのJS、CSS を定義したベース Navbar ヘッダーのナビバー Navbarを使わないもの ログイン など Navbarを使うもの CMSの各種ページ base.html base_navi.html login.html などindex.html など
  37. 37. Bootstrap 使い方としては • 一覧系のページは、Bootstrapのclassを使って普通に書く • フォーム系のページは、django-bootstrap-form https://github.com/tzangms/django-bootstrap-form を使う $ pip install django-bootstrap-form
  38. 38. base.html Bootstrapの例 {% 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> Bootstrap の JS、CSSを記述する ベースとなるテンプレート
  39. 39. base_navi.html Bootstrapの例 {% extends "base.html" %} ! {% block navbar %} <nav class="navbar navbar-default" role="navigation"> <div class="container-fluid"> ← base.html を継承 ← base.html の navbar ブロックを置き換え <!-- 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 %} Bootstrap の Navbar のみを定義
  40. 40. index.html {% extends “base_navi.html" %} ! {% block title %}書籍の一覧{% endblock title %} ! {% block content %} ← base_navi.html を継承 ← base.html の title ブロックを置き換え ← base.html の 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 %} Bootstrapの例 ↑ 一覧系は Bootstrap の class を使って普通に書く
  41. 41. book_edit.html {% 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 %} Bootstrapの例 ← django-bootstrap-form を使っているので   Form の項目を Bootstrap 形式で展開してくれる
  42. 42. django-bootstrap-formのテクニック form を丸ごと出す {{ form|bootstrap_horizontal }} 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 でやっていることを真似ること
  43. 43. 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 一覧
  44. 44. 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', )
  45. 45. CRUDの書き方 views.py 削除 def book_del(request, book_id): '''書籍の削除''' book = get_object_or_404(Book, pk=book_id) book.delete() return redirect('cms:book_list') 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'), # 削除 )
  46. 46. Listページの書き方 • django.views.generic.list.ListView を使っておくと、ページネートが簡単 • Bootstrapのページネート部品とも相性がいい views.py 一覧 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)
  47. 47. Listページの書き方 impression_list.html のページング部分 {% 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 %}
  48. 48. Listページの書き方 ページングの表示例 この部分
  49. 49. APIの実装
  50. 50. Django REST framework とかもあるけど・・    
  51. 51. レスポンスを自前で JSONで書く
  52. 52. 辞書を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')
  53. 53. 項目が順不同になる・・ { "org_list": [ { "id": 1, "name": "Japan System Laboratory" }, { "id": 2, "name": "GEEKLAB.NAGANO" } ], "status": "ok", "message": "Success", "response_code": "000" }
  54. 54. アプリ開発者から文句 を言われるw・・
  55. 55. なので・・
  56. 56. OrderedDict()で順序付き辞書にする 順序付き辞書を使いましょう!(Python 2.7~) from collections import OrderedDict def 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')
  57. 57. ちゃんとコード通りSortされる { "status": "ok", "response_code": "000", "message": "Success", "org_list": [ { "name": "Japan System Laboratory", "id": 1 }, { "name": "GEEKLAB.NAGANO", "id": 2 } ] }
  58. 58. Postの場合 • 簡単なデータの場合は、CMSのForm受信と同じものを 書いて、スマホ側では http form post を模倣してもら う。 ◦ こうすることによって、データのエラーチェックは、 フォームのバリデーションの仕組みが使えます。 ◦ 正常かエラーかは、JSONで結果を返すようにしま す。 ! • 繰り返しがある複雑なデータは、スマホ側からJSONを POSTしてもらい、json.loads() でデコードする
  59. 59. 簡単なデータの場合 formの作成 #ログインフォーム class MemberLoginForm(forms.Form): email = forms.CharField(label='email', max_length=255) password = forms.CharField(label='password', max_length=255) ファンクションの作成 @csrf_exempt def 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))
  60. 60. 簡単なデータの場合
  61. 61. 複雑なデータの場合 formの作成(JSONのテンプレをplaceholderで表示してあげると親切) 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)
  62. 62. 複雑なデータの場合 この部分
  63. 63. 複雑なデータの場合 ファンクションの作成(json.loadsでJSONを解析) @csrf_exempt def 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))
  64. 64. PUSH通知
  65. 65. to iOS
  66. 66. APNs (Apple Notification Service)
  67. 67. PyAPNs • 最新バージョン 1.1.2(今のところPython3は未対応) https://github.com/djacobs/PyAPNs • 事前に証明書ファイル・キーファイルを作成しておく $ pip install git+git://github.com/djacobs/PyAPNs.git ※普通に入れると、期待したものが入らない可能性があるので、  GitHubからインストール
  68. 68. 1デバイスへのPUSH通知 from apns import APNs, Frame, Payload def 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)
  69. 69. 複数デバイスへのPUSH通知 最新の形式(frame形式?) from apns import APNs, Payload def 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)
  70. 70. feedbackで返された トークンは、削除する for (token_hex, fail_time) in apns.feedback_server.items(): #未使用のデバイストークンを削除 for token in DeviceToken.objects.filter(token=token_hex): token.delete() !
  71. 71. 実装上のポイント • デバイストークンを収集する仕組み > API  *ユーザの複数端末持ちを考慮 • ペイロードのサイズ制限は256バイト > 冗長した メッセージは「・・・」等で調整
  72. 72. to Android
  73. 73. GCM (Google Cloud Message)
  74. 74. python-gcm • 最新バージョンは 0.1.5 https://github.com/geeknam/python-gcm • APIキーを事前にGoogle API Consoleから取得 $ pip install python-gcm
  75. 75. 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()
  76. 76. 実装上のポイント • デバイストークンを収集する仕組み > API  *ユーザの複数端末持ちを考慮(APNsと同様) • ペイロードのサイズ制限は4096バイト > 気にし なくて良いレベル
  77. 77. ログイン パスワードの暗号化 • サーバ/スマホ間のパスワード通信を暗号化したい • iOS/Android/Python で共通で暗号化/復号化できる ベストなプロトコルは何か • AESがよい(AES ECBモード) AESの暗号化はバイナリ値になるのでBASE64に変換 • pycrypto を使う https://www.dlitz.net/software/pycrypto/ $ pip install pycrypto
  78. 78. ログイン パスワードの暗号化 AES 復号化の部分 from Crypto.Cipher import AES from 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')
  79. 79. ログイン パスワードの暗号化 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)
  80. 80. ログイン連携 • Twitter/Facebook などの OAuth 2.0 連携は、 python-social-authで用意されている - https://github.com/omab/python-social-auth • OpenID Connect でログイン連携したい - 今後多くなると思われ • Yahoo Janan! の OpenID Connect (YConnect) の胸を借りる - 公開してくれている Yahoo Janan! に感謝を!
  81. 81. ログイン連携 • python-social-auth の拡張モジュールを書く - 本家でもOpenID Connectは未対応? - 自分で書くことにした ‣ ベースはOAuth2.0で行ける ‣ OpenIDっぽいnonceの処理がある ‣ JWT (JSON Web Token)のデコードを追加 • サンプルはGitHub Gistを参照(長いので割愛) https://gist.github.com/kakky/6809432
  82. 82. アプリケーションを公開する
  83. 83. AWSで公開する iOS/Android アプリAmazon EC2 Mobile Client DBサーバー (MySQL) Amazon RDS 画像、音声、映像 Amazon S3 Email Amazon SES AWS SDK for Python (boto) 普通に SMTPサーバー として指定 IPアドレス指定 量が少ない場合は GMail、Google Apps で済ませてしまう場合もあり
  84. 84. botoによるAmazon S3連携 画像ファイル等をS3に追い出すために、まずはこれ $ pip install boto S3 の bucket と key の関係 • URLに変換する際に、bucketはホスト名の一部になるため、全世界で 一意にする • keyの部分は、/を使って任意にフォルダ的なものを作ることができる • bucket=my-bucket-name、key=path/to/image.jpg とすると、 以下のようなURLを生成できる https://my-bucket-name.s3.amazonaws.com/path/to/image.jpg
  85. 85. botoによるAmazon S3連携 S3へのアップロードと、パブリックなURLの取得 import boto, mimetypes, os from 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 に置いてあると仮定
  86. 86. • Djangoアプリケーションのデプロイは以下を使用 - nginx : Webサーバ - uWSGI : アプリケーション コンテナ サーバ ‣ 姉妹サービスを同一ホストで公開することも踏まえ ‣ emperor/vassals(皇帝/家臣)モードを使用 uWSGI vassal uWSGI vassal nginx nginx + uWSGI uWSGI emperor 皇帝 家臣/家来? サービス1 仮想ホスト1 サービス2 仮想ホスト2 upstream 起動
  87. 87. 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; } }
  88. 88. uWSGI emperorの設定 # /etc/uwsgi.yaml uwsgi: 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 にて起動できるようにしているが、長いので割愛(すみません)
  89. 89. uWSGI emperorの設定 とはいえ、後からスライドを見て、コピペしたい人用に /etc/rc.d/init.d/uwsgi のスクリプトを貼っておきます #!/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 C!ONFFILE="/etc/uwsgi.yaml" if [ -f /etc/sysconfig/uwsgi ]; then . /etc/sysconfig/uwsgi f!i prog=uwsgi uwsgi=${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=2 e!sac exit $RETVAL 今、見えなくても怒らないで (́・ω・`)
  90. 90. uWSGI vassalsの設定 # /etc/uwsgi/vassals/myservice_uwsgi.yaml uwsgi: 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 の方が、よろしくやっ てくれた感がある
  91. 91. uWSGIでオートリロード • 開発サーバと同じく、コードをデプロイしたら、 自動的に再起動して反映してほしい • 果たして、プロダクションでそれをやっていいかは 議論の余地があるが、便利なので設定する
  92. 92. uWSGIでオートリロード プロジェクトのディレクトリ直下に、uwsgi_autoreload.py というコードを置く # -*- coding: utf-8 -*- # nginx + uWSGI で実行した時、ソースコードの変更を検知して、uWSGIを再起動する # # 注) import uwsgi は uWSGI 配下で実行した時のみ参照できる # from uwsgidecorators も同様 # どちらもローカル開発時は Unresolved import のままでよい ! import uwsgi from uwsgidecorators import timer from django.utils import autoreload ! @timer(3) # 3秒ごとに呼ばれる def change_code_gracefull_reload(sig): if autoreload.code_changed(): print(‘code change detected. autoreload ——————————————————————‘) uwsgi.reload()
  93. 93. uWSGIでオートリロード uWSGI vassals の設定ファイルで指定する # /etc/uwsgi/vassals/myservice_uwsgi.yaml uwsgi: : : import: uwsgi_autoreload : :
  94. 94. Qiitaにチュートリアル書きました http://qiita.com/kaki_k/items/511611cadac1d0c69c54
  95. 95. Qiitaにチュートリアル書きました • 「Django入門」でググると、 一番上に出てきてビビリます • Djangoを使う人の裾野を広げたいと思い 書きました。 • 公式チュートリアルと合わせて、 新しい人材の育成にご活用下さい。
  96. 96. まとめ • コード部分は小さい字が多くてすみません。 • スライドは後ほど公開しますので、小さくて見えな かった部分は、後で見返して下さい。 • ということで、 スマートフォンとの連携案件を、 Djangoを使ってどんどん作りましょう!
  97. 97. ご清聴ありがとうございました

×