Python Celery Supervisor

Contents

  1. Tips
    1. Apache (mod_wsgi) を用いたデプロイ
      1. 関連リンク
    2. インタラクティブ環境を使う
    3. プロジェクトのルートディレクトリを (project_name)/(project_name)/settings.py で使う
    4. Cross Site Request Forgery (CSRF)
      1. CSRFトークンを埋め込む
      2. csrf_token なしのPOSTリクエストを許す
    5. adminコマンド (./manage.py hogehoge) を追加する
    6. プロダクションでの静的ファイルの配置 (cssやjsなど)
      1. 基本的な話
      2. 例: bootstrapを既存のプロジェクトに導入する
      3. 参考
      4. 自分的な課題
    7. djcelery でAsyncTaskのwait()が返ってこないとき
    8. logging
      1. asctimeのフォーマットを変更する
    9. reset, flush
    10. GitHubからSecret付きWebHookを受け取る
    11. Django + venv + Celery + Supervisor
    12. tornado + Djangoの例
  2. Django 2.2
    1. settings.py
  3. Django 1.7
    1. 絵文字 + MySQL
    2. マイグレーション
    3. pyvenvを用いてPython 3.4 + Django 1.7環境のvenvを作る
    4. 1_6.W001 ... It looks like this project was generated using Django 1.5 or earlier.
  4. パフォーマンス
    1. キャッシュ
    2. ORマッパー
    3. 計測ツール系
  5. 追加ライブラリ
    1. python-social-authによるソーシャル認証
      1. 認証時の動作
      2. 既存のユーザが同じemailアドレスを使っているようならそのオブジェクトを再利用する
      3. ログイン前のTwitterアカウントを前もってUserに紐付ける
      4. 雑多なメモ
    2. django_openid_authによる OpenID 認証 (deprecated)
    3. mod_auth_tkt: 「チケット」を介したApacheとの連携認可
    4. X-SendFile
    5. django-oauth-toolkit
    6. django-debug-toolbar
    7. django-compressor
    8. django-replicated
  6. ユニットテスト Tips
    1. HTTP GETして200が返ってくるか調べる
    2. ユーザを追加
    3. HTTPアクセスしてcontent typeが text/xml であることを確かめる
    4. django.test.Client でアクセステストをする際のヘッダを追加する
    5. assertHogeHoge チートシート (予定地)
  7. (Deprecated) Introductory Chapter (django 初級者)
    1. django 入門 (コマンドを実行すること無くなんとなく過程だけ把握したい人向け)
  8. Quick Start
    1. 初期設定の変更
      1. データベースの生成
      2. Model とデータベースの作成
      3. View の作成
      4. Apache上で実行を確認する
      5. ログをファイルに保存する
  9. misc リンク

Tips

Apache (mod_wsgi) を用いたデプロイ

(Debian wheezy + Apache 2.2の例)

WSGIDaemonProcess test_user user=www-data group=www-data processes=1 threads=2 \
  maximum-requests=2 umask=0007 python-path=/opt/test_project
WSGIProcessGroup test_user

WSGIScriptAlias /test_project /path/to/test_project/test_app/wsgi.py
<Directory /path/to/test_project/test_app>
    <Files wsgi.py>
        Order deny,allow
        Allow from all
    </Files>
</Directory>

WSGIDaemonProcess test_user user=www-data group=www-data processes=1 threads=2 \
  maximum-requests=2 umask=0007 python-path=/opt/test_project
WSGIDaemonProcess custom_user user=www-data group=www-data processes=1 threads=2 \
  maximum-requests=2 umask=0007 \
  python-path=/opt/custom_project:/opt/custom_project/venv/lib/python2.7/site-packages

# デフォルトで test_user を使う設定をApacheの環境変数へ含める
# なおこれはOSの環境変数とは異なるもの。djangoからは取得できない
SetEnv PROCESS_GROUP test_user
WSGIProcessGroup %{ENV:PROCESS_GROUP}

WSGIScriptAlias /test_user /path/to/test_project/test_app/wsgi.py
<Directory /path/to/test_project/test_app>
    <Files wsgi.py>
        Order deny,allow
        Allow from all
    </Files>
</Directory>

WSGIScriptAlias /custom_project /path/to/custom_project/custom_app/wsgi.py
<Directory /path/to/custom_project/custom_app>
    <Files wsgi.py>
        # ここでプロセスグループを切り替えるべく設定を変更
        SetEnv PROCESS_GROUP custom_user
        Order deny,allow
        Allow from all
    </Files>
</Directory>

virtualenv についてはPythonを参考

関連リンク

インタラクティブ環境を使う

> ./manage.py shell

ipython入れると履歴とか管理してくれるから楽。ただし回避できない警告が毎回出てくる。かなしい。

プロジェクトのルートディレクトリを (project_name)/(project_name)/settings.py で使う

django 1.7等の標準settings.pyには既に準備がある。

# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
import os
BASE_DIR = os.path.dirname(os.path.dirname(__file__))

Cross Site Request Forgery (CSRF)

CSRFトークンを埋め込む

フォームで csrf_token を使う

<form action="{% url 'polls:vote' poll.id %}" method="post">
{% csrf_token %}
{% for choice in poll.choice_set.all %}
    <input type="radio" name="choice" id="choice{{ forloop.counter }}" value="{{ choice.id }}" />
    <label for="choice{{ forloop.counter }}">{{ choice.choice_text }}</label><br />
{% endfor %}
<input type="submit" value="Vote" />
</form>

csrf_token なしのPOSTリクエストを許す

外部サービスから起動されるフローでは必須 (例: Twilio, githubの Post-Receive Hooks)

https://docs.djangoproject.com/en/dev/ref/contrib/csrf/

全部disableする場合には

MIDDLEWARE_CLASSES = (
    'django.middleware.common.CommonMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',       <---- コメントアウトする
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    # Uncomment the next line for simple clickjacking protection:
    # 'django.middleware.clickjacking.XFrameOptionsMiddleware',
)

1つだけの場合

from django.views.decorators.csrf import csrf_exempt
from django.http import HttpResponse

@csrf_exempt
def my_view(request):
    return HttpResponse('Hello world')

adminコマンド (./manage.py hogehoge) を追加する

プロダクションでの静的ファイルの配置 (cssやjsなど)

基本的な話

例: bootstrapを既存のプロジェクトに導入する

例としてmysiteという名前のプロジェクトを考える。

状況は以下を仮定する。

bootstrap自体の詳細な利用方法はこの説明の範囲外とする。 ここでは、以下のような構成 (bootstrap 3.0.0) のディレクトリをどこに配置するかを考える

bootstrap
|-- css
|   |-- bootstrap-theme.css
|   |-- bootstrap-theme.min.css
|   |-- bootstrap.css
|   `-- bootstrap.min.css
|-- fonts
|   |-- glyphicons-halflings-regular.eot
|   |-- glyphicons-halflings-regular.svg
|   |-- glyphicons-halflings-regular.ttf
|   `-- glyphicons-halflings-regular.woff
`-- js
    |-- bootstrap.js
    `-- bootstrap.min.js

この場合、少なくともこのプロジェクト内の二つのアプリケーションdjango.contrib.adminmysite両方共静的ファイルを持つ。

django.contrib.adminは暗黙にadmin.cssといったCSSを要求するし、 mysiteはbootstrapのファイル郡をどこかに用意しなければならない。

まず、これらの静的ファイルを一箇所に集めるための設定をする。

mysiteにbootstrapを追加したいだけだから、今回は /opt/mysite/mysite/static/ ディレクトリ配下にbootstrap/を置けば良い

次に、集めたファイルを置くディレクトリをsettings.STATIC_ROOTに記述する。

これはmysiteの置かれているディレクトリ(/opt/mysite)外部でも良いが、 現在のユーザから書き込め、Apacheから読み込み可能である必要がある。

今回は /opt/mysite/static_all というディレクトリを使うことにする

# PROJECT_ROOTは "/opt/mysite" と同じ
import os.path
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
STATIC_ROOT = os.path.join(PROJECT_ROOT, 'static_all')

この状態で ./manage.py collectstatic を実行する

/opt/mysite> ./manage.py collectstatic

You have requested to collect static files at the destination
location as specified in your settings.

This will overwrite existing files!
Are you sure you want to do this?

Type 'yes' to continue, or 'no' to cancel: yes
Copying '/usr/lib/python2.7/dist-packages/django/contrib/admin/static/admin/css/rtl.css'
Copying '/usr/lib/python2.7/dist-packages/django/contrib/admin/static/admin/css/changelists.css'
Copying '/usr/lib/python2.7/dist-packages/django/contrib/admin/static/admin/css/dashboard.css'
Copying '/usr/lib/python2.7/dist-packages/django/contrib/admin/static/admin/css/login.css'
Copying '/usr/lib/python2.7/dist-packages/django/contrib/admin/static/admin/css/ie.css'
Copying '/usr/lib/python2.7/dist-packages/django/contrib/admin/static/admin/css/base.css'
...

ここで/opt/mysite/static_all に関連する全ての静的ファイルが集約されているのを確認すること。

最後に集約された静的ファイルをApacheが外部に公開する際のURLをsettings.STATIC_URLに記述する。

STATIC_URL = '/mysite_static/'

STATIC_URLはテンプレートに {% static "bootstrap/css/bootstrap.min.css" %}  などと記述した時に接頭辞になる。

今回のケースでは、展開結果は/mysite_static/css/bootstrap.min.css になる。

http://(host名)/mysite_static/opt/mysite/static_all に関連付ける設定は、 Apacheの設定ファイルに記述する。

(これはApacheの関連する設定として記述すること。djangoの設定ではない)
Alias /mysite_static /opt/mysite/static_all

http://(host名)/mysite_static/bootstrap/css/bootstrap.min.css 等にアクセスして cssが読めることを確認する。

これで外部への静的ファイルの公開は終わった。

後はテンプレートの内部で実際に通常のHTMLの一部として記述し、cssやjsが利用できることを確認する。

参考

自分的な課題

この方式だとdjangoの認証・認可後に静的ファイルを提供する方法がない。

なおApacheのBASIC認証をdjangoの認証と絡める方法については django のドキュメントに若干の記載がある (django 1.5)

djcelery でAsyncTaskのwait()が返ってこないとき

MySQL(+ innodb)のレベルでREPEATED-READになっていないか確認する。

参考

djceleryはMySQLを利用している。innodbを用いている場合さらにREPEATED-READになるため、 結果がある意味キャッシュされたかのような挙動を引き起こす。

logging

asctimeのフォーマットを変更する

LOGGING = {
    ...

    'formatters': {
        'default': {
            'format': ('%(levelname)s %(asctime)s'
                       ' %(module)s %(lineno)s %(message)s'),
            'datefmt': '%Y-%m-%d %H:%M:%S'
        },
    },

    ...

reset, flush

GitHubからSecret付きWebHookを受け取る

import hashlib
import hmac

...
    if content_type == 'application/json':
        payload = request.body
    else:
        payload = request.POST.get('payload')
    signature = request.META.get('HTTP_X_HUB_SIGNATURE')
    if signature:
        hasher = hmac.new(secret, payload, hashlib.sha1)
        logger.debug('Signature : {}'.format(signature))
        logger.debug('Calculated: sha1={}'.format(hasher.hexdigest()))

Django + venv + Celery + Supervisor

/usr/bin/python /opt/griflet/manage.py celery worker --time-limit=1200 -c 1 -n build_book@nagasode --loglevel=DEBUG --workdir=/opt/griflet/ -Q build_book --logfile=/var/log/celery/build_book.log --pidfile=/var/run/celery/build_book.pid
/usr/bin/python /opt/griflet/manage.py celery worker --time-limit=300 -c 1 -n manage_project@nagasode --loglevel=DEBUG --workdir=/opt/griflet/ -Q manage_project --logfile=/var/log/celery/manage_project.log --pidfile=/var/run/celery/manage_project.pid
/usr/bin/python /opt/griflet/manage.py celery worker --time-limit=300 -c 1 -n draft_build@nagasode --loglevel=DEBUG --workdir=/opt/griflet/ -Q draft_build --logfile=/var/log/celery/draft_build.log --pidfile=/var/run/celery/draft_build.pid
/usr/bin/python /opt/griflet/manage.py celery worker --time-limit=1200 -c 1 -n build_book@nagasode --loglevel=DEBUG --workdir=/opt/griflet/ -Q build_book --logfile=/var/log/celery/build_book.log --pidfile=/var/run/celery/build_book.pid
/usr/bin/python /opt/griflet/manage.py celery worker --time-limit=300 -c 1 -n manage_project@nagasode --loglevel=DEBUG --workdir=/opt/griflet/ -Q manage_project --logfile=/var/log/celery/manage_project.log --pidfile=/var/run/celery/manage_project.pid
/usr/bin/python /opt/griflet/manage.py celery worker --time-limit=300 -c 1 -n draft_build@nagasode --loglevel=DEBUG --workdir=/opt/griflet/ -Q draft_build --logfile=/var/log/celery/draft_build.log --pidfile=/var/run/celery/draft_build.pid

tornado + Djangoの例

Django 2.2

settings.py

"""
Django settings for myproject project.

Generated by 'django-admin startproject' using Django 2.2.6.

For more information on this file, see
https://docs.djangoproject.com/en/2.2/topics/settings/

For the full list of settings and their values, see
https://docs.djangoproject.com/en/2.2/ref/settings/
"""

import os

# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))


# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = '$1b&j-&10d&o#mu-q!=i2ud(i!9o5zuq899+99#n%7@^4f*z16'

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True

ALLOWED_HOSTS = []


# Application definition

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

ROOT_URLCONF = 'myproject.urls'

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

WSGI_APPLICATION = 'myproject.wsgi.application'


# Database
# https://docs.djangoproject.com/en/2.2/ref/settings/#databases

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
    }
}


# Password validation
# https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators

AUTH_PASSWORD_VALIDATORS = [
    {
        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
    },
]


# Internationalization
# https://docs.djangoproject.com/en/2.2/topics/i18n/

LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_L10N = True
USE_TZ = True

# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/2.2/howto/static-files/

STATIC_URL = '/static/'

Django 1.7

絵文字 + MySQL

マイグレーション

例えば空のmodelsに対して

from django.db import models

class Entry(models.Model):
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

と新たにEntryというモデルを書いたとする。 この状態でmigrate (昔で言うsyncdb) をしようとすると

> ./manage.py migrate
Operations to perform:
  Apply all migrations: admin, contenttypes, auth, sessions
Running migrations:
  No migrations to apply.
  Your models have changes that are not yet reflected in a migration, and so won't be applied.
  Run 'manage.py makemigrations' to make new migrations, and then re-run 'manage.py migrate' to apply them.

と、怒られる。makemigrationsによってDBに対する変更をどう行うかのファイルを生成させる

> ./manage.py makemigrations
Migrations for 'firstapp':
  0001_initial.py:
    - Create model Entry

ここでmigrations/0001_initial.pyが自動生成される。今回は例えば以下の通り

# -*- coding: utf-8 -*-
from __future__ import unicode_literals

from django.db import models, migrations


class Migration(migrations.Migration):

    dependencies = [
    ]

    operations = [
        migrations.CreateModel(
            name='Entry',
            fields=[
                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
                ('created_at', models.DateTimeField(auto_now_add=True)),
                ('updated_at', models.DateTimeField(auto_now=True)),
            ],
            options={
            },
            bases=(models.Model,),
        ),
    ]

この定義が書かれた時点で始めて migrate が実行できる。

> ./manage.py migrate
Operations to perform:
  Apply all migrations: admin, contenttypes, sessions, auth, firstapp
Running migrations:
  Applying firstapp.0001_initial... OK

このマイグレーションがどのようなSQLに対応するかを調べるには sqlmigrate コマンドを使う。 今回はfirstappというプロジェクト内のアプリケーションを使っているので

> ./manage.py sqlmigrate firstapp 0001_initial
BEGIN;
CREATE TABLE "firstapp_entry" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "created_at" datetime NOT NULL, "updated_at" datetime NOT NULL);

COMMIT;

pyvenvを用いてPython 3.4 + Django 1.7環境のvenvを作る

/opt> pyenv versions
  system
* 3.4.0 (set by /home/dmiyakawa/.pyenv/version)
/opt> python --version
Python 3.4.0
/opt> pyvenv python3.4.0-django1.7
/opt> source python3.4.0-django1.7/bin/activate          (bash, zshの場合)
(python3.4.0-django1.7) /opt> pip install django
...
(python3.4.0-django1.7) /opt> pip freeze
Django==1.7
(python3.4.0-django1.7) /opt> rehash     (zsh の場合)
(python3.4.0-django1.7) /opt> django-admin.py --version
1.7

ここまでで Python 3.4.0 と Django 1.7の構成が出来上がっている。

(python3.4.0-django1.7) /opt> django-admin.py startproject helloworld_django17

pyvenvの環境としてpython3.4.0-django1.7をそのまま使ってよければこれでおしまい。

プロジェクトごとにPython環境を分けるのであればさらに

(python3.4.0-django1.7) /opt> cd helloworld_django17
(python3.4.0-django1.7) /opt/helloworld_django17> pyvenv --copies venv
(python3.4.0-django1.7) /opt/helloworld_django17> deactivate
/opt/helloworld_django17> source venv/bin/activate
(venv) /opt/helloworld_django17> pip install django

1_6.W001 ... It looks like this project was generated using Django 1.5 or earlier.

> python manage.py check
System check identified some issues:

WARNINGS:
?: (1_6.W001) Some project unittests may not execute as expected.
        HINT: Django 1.6 introduced a new default test runner. It looks like this project was generated using Django 1.5 or earlier. You should ensure your tests are all running & behaving as expected. See https://docs.djangoproject.com/en/dev/releases/1.6/#new-test-runner for more information.

1.6でテストの探され方が変わっているけどお前のテスト大丈夫か、と聞いている。 まず、全てのテストが期待通りに実行されるか調べること。

これを確認した上で、settings.pyに以下を追加する。なお、標準のクラスを明示しているだけ。

TEST_RUNNER = 'django.test.runner.DiscoverRunner'

仮に古いテストの挙動を残したい場合は、以下と記述すると deprecated のまましばらく残る (1.8まで)

TEST_RUNNER = 'django.test.simple.DjangoTestSuiteRunner'

パフォーマンス

キャッシュ

ORマッパー

計測ツール系

追加ライブラリ

python-social-authによるソーシャル認証

認証時の動作

例: /usr/local/lib/python2.7/dist-packages/social/apps/django_app/urls.py

urlpatterns = patterns('social.apps.django_app.views',
    # authentication / association
    url(r'^login/(?P<backend>[^/]+)/$', 'auth',
        name='begin'),
    url(r'^complete/(?P<backend>[^/]+)/$', 'complete',
        name='complete'),
    # disconnection
    url(r'^disconnect/(?P<backend>[^/]+)/$', 'disconnect',
        name='disconnect'),
    url(r'^disconnect/(?P<backend>[^/]+)/(?P<association_id>[^/]+)/$',
        'disconnect', name='disconnect_individual'),
)

例: /usr/local/lib/python2.7/dist-packages/social/apps/django_app/me/models.py

class UserSocialAuth(Document, DjangoUserMixin):
    """Social Auth association model"""
    user = ReferenceField(USER_MODEL)
    provider = StringField(max_length=32)
    uid = StringField(max_length=255, unique_with='provider')
    extra_data = DictField()

    ...

既存のユーザが同じemailアドレスを使っているようならそのオブジェクトを再利用する

パイプラインにsocial.pipeline.social_auth.associate_by_emailを挟む。セキュリティ的には微妙?

SOCIAL_AUTH_PIPELINE = (
    'social.pipeline.social_auth.social_details',
    'social.pipeline.social_auth.social_uid',
    'social.pipeline.social_auth.auth_allowed',
    'social.pipeline.social_auth.associate_by_email',
    'social.pipeline.user.get_username',
    'social.pipeline.user.create_user',
    'social.pipeline.social_auth.associate_user',
    'social.pipeline.social_auth.load_extra_data',
    'social.pipeline.user.user_details'
)

ただし相手にするプロバイダによっては注意が必要。 プロバイダ側でemailを確認なしに変更できたりすると、 associate_by_emailの段階で任意のアカウントの乗っ取りを行えてしまう。

def associate_by_email(strategy, details, user=None, *args, **kwargs):
    """
    Associate current auth with a user with the same email address in the DB.

    This pipeline entry is not 100% secure unless you know that the providers
    enabled enforce email verification on their side, otherwise a user can
    attempt to take over another user account by using the same (not validated)
    email address on some provider.  This pipeline entry is disabled by
    default.
    """
    if user:
        return None

    email = details.get('email')
    if email:
        # Try to associate accounts registered with the same email address,
        # only if it's a single object. AuthException is raised if multiple
        # objects are returned.
        users = list(strategy.storage.user.get_users_by_email(email))
        if len(users) == 0:
            return None
        elif len(users) > 1:
            raise AuthException(
                strategy.backend,
                'The given email address is associated with multiple accounts'
            )
        else:
            return {'user': users[0]}

ログイン前のTwitterアカウントを前もってUserに紐付ける

screen_name (e.g. @amedama) はUserSocialAuthのuidではない (参考: http://syncer.jp/twitter-screenname-userid-converter)

uidを調べるところから行う場合は例えば以下のようにする。

import requests
from requests_oauthlib import OAuth1

url  = 'https://api.twitter.com/1.1/users/show.json?screen_name={}'.format(screen_name)
auth = OAuth1(TWITTER_APP_KEY, TWITTER_APP_SECRET,
              TWITTER_OAUTH_TOKEN, TWITTER_OAUTH_SECRET)
res = requests.get(url, auth=auth)
twitter_uid = json.loads(res.text)['id']
UserSocialAuth.create_social_auth(user, twitter_uid, 'twitter')

雑多なメモ

使われるpathは^login/, ^complete/, ^disconnect/

> cat /usr/local/lib/python2.7/dist-packages/social/apps/django_app/urls.py
"""URLs module"""
try:
    from django.conf.urls import patterns, url
except ImportError:
    # Django < 1.4
    from django.conf.urls.defaults import patterns, url


urlpatterns = patterns('social.apps.django_app.views',
    # authentication / association
    url(r'^login/(?P<backend>[^/]+)/$', 'auth',
        name='begin'),
    url(r'^complete/(?P<backend>[^/]+)/$', 'complete',
        name='complete'),
    # disconnection
    url(r'^disconnect/(?P<backend>[^/]+)/$', 'disconnect',
        name='disconnect'),
    url(r'^disconnect/(?P<backend>[^/]+)/(?P<association_id>[^/]+)/$',
        'disconnect', name='disconnect_individual'),
)

django_openid_authによる OpenID 認証 (deprecated)

注: django-social-auth の方が良い気がする。

mod_wsgi によってURL上のルートが変わっている場合には上のインストラクション通りでは動作しないので注意。

settings.py の一部を示す。

from django.core.urlresolvers import reverse_lazy
AUTHENTICATION_BACKENDS = (
  'django_openid_auth.auth.OpenIDBackend',
  'django.contrib.auth.backends.ModelBackend',
)
OPENID_CREATE_USERS = True
# OpenID Simple Registration
OPENID_UPDATE_DETAILS_FROM_SREG = True
# Attribute Exchange
OPENID_UPDATE_DETAILS_FROM_AX = True
# Just using something like "/openid/login/" won't work well with wsgi.
# Reason:
# Assume we have the following config in Apache, which kicks this project.
#
# WSGIScriptAlias /c84_ci /opt/c84_ci/c84_ci/wsgi.py
#
# In that case, we need to set LOGIN_URL to "/c84_ci/openid/login/"
# instead of "/openid/login". More in general we should not manually
# embed urls but use django's nice lookup functionality.
# reverse() is usually nice solution for views.py.
#
# In this exact case, however, reverse() won't work because URLConf
# is not ready when loading this file.
LOGIN_URL = reverse_lazy('openid-login')
LOGIN_REDIRECT_URL = reverse_lazy('home')
OPENID_SSO_SERVER_URL = 'https://www.google.com/accounts/o8/id'

このモジュール、細部で微妙なところがあるっぽい。例えば、urls.py の

    url(r'^openid/', include('django_openid_auth.urls')),

で namespace を使って url "openid:openid-login" という形式で動作させようとすると、うまく行かない。後で自分で直す……。

mod_auth_tkt: 「チケット」を介したApacheとの連携認可

X-SendFile

(httpd.conf相当のファイルで)

XSendFile On
XSendFilePath /opt/yourapp/data

とすると/opt/yourapp/data配下のファイルのみ対象となる。

django-oauth-toolkit

(urls.py)
from django.conf.urls import patterns, include, url
from django.contrib import admin

urlpatterns = patterns(
    '',
    url(r'^admin/', include(admin.site.urls)),
    url(r'^accounts/login/$', 'django.contrib.auth.views.login',
        {'template_name': 'oauthtest/login.djhtml'}),
    url(r'^o/', include('oauth2_provider.urls',
                        namespace='oauth2_provider')),
)

django-debug-toolbar

django-compressor

django-replicated

ユニットテスト Tips

django.test.utils.setup_test_environment() はDBをモックアウトしないが、Viewのテストでは普通にデータベースは自動的にモックのものが与えられる。

def create_poll(question, days):
    return Poll.objects.create(question=question,
        pub_date=timezone.now() + datetime.timedelta(days=days))

create() はここではデータベースへの書き込みも行う (参考)

HTTP GETして200が返ってくるか調べる

urls.py, views.py に "home" というトップページの設定が記述されているとして

from django.core.urlresolvers import reverse
from django.test import TestCase
..
class ViewTest(TestCase):
    def test_home(self):
        response = self.client.get(reverse('home'))
        self.assertEqual(200, response.status_code)

これをtests.py に含めて

> ./manage.py test (app_name)

ユーザを追加

from django.contrib.auth.models import User

..
    def test_misc(self):
        user = User.objects.create(username='user1', email='user1@example.com')

HTTPアクセスしてcontent typeが text/xml であることを確かめる

django.test.Client でアクセステストをする際のヘッダを追加する

例として前述したgithubのwebhookを受け取るサーバのテストを考える。

2014-06-17時点ではX-Github-EventX-Hub-SignatureがHTTPヘッダに含まれる。 この場合、Djangoのサーバ側ではrequest.META.get('HTTP_X_GITHUB_EVENT')request.META.get('HTTP_X_HUB_SIGNATURE')として値を受け取ることが出来る。

これをdjango.test.Clientのpost()で送る際、django 1.6であれば単に引数に含めてしまえば良い。

        hasher = hmac.new('TestSecret', payload_str, hashlib.sha1)
        response = self.client.post(
            url, data={'payload': payload_str},
            HTTP_X_GITHUB_EVENT='ping',
            HTTP_X_HUB_SIGNATURE='sha1={}'.format(hasher.hexdigest()))
        self.assertEqual(response.status_code, 200)

assertHogeHoge チートシート (予定地)

Python 2.7, django 1.3, 1.4。かなりいい加減なので正確性については期待しないこと

assertEqual(a, b)
assertNotEqual(a, b)
assertTrue(x)
assertFalse(x)
assertIs(a, b)
assertIsNot(a, b)
assertIsNone(x)
assertIsNotNone(x)
assertIn(a, b)
assertNotIn(a, b)
assertIsInstance(a, b)
assertNotIsInstance(a, b)

SimpleTestCase.assertRaisesMessage(expected_exception, expected_message, callable_obj=None, *args, **kwargs) 1.4
SimpleTestCase.assertFieldOutput(self, fieldclass, valid, invalid, field_args=None, field_kwargs=None, empty_value=u'')
TestCase.assertContains(response, text, count=None, status_code=200, msg_prefix='', html=False)
TestCase.assertNotContains(response, text, status_code=200, msg_prefix='', html=False)
TestCase.assertFormError(response, form, field, errors, msg_prefix='')
TestCase.assertTemplateUsed(response, template_name, msg_prefix='')
TestCase.assertTemplateNotUsed(response, template_name, msg_prefix='')
TestCase.assertRedirects(response, expected_url, status_code=302, target_status_code=200, msg_prefix='')
TestCase.assertQuerysetEqual(qs, values, transform=repr, ordered=True)
TestCase.assertNumQueries(num, func, *args, **kwargs)
SimpleTestCase.assertHTMLEqual(html1, html2, msg=None)
SimpleTestCase.assertHTMLNotEqual(html1, html2, msg=None)
SimpleTestCase.assertXMLEqual(xml1, xml2, msg=None)
SimpleTestCase.assertXMLNotEqual(xml1, xml2, msg=None)

(Deprecated) Introductory Chapter (django 初級者)

だいぶ古い(Django 1.5の頃の情報)。後で消すか直す

django 入門 (コマンドを実行すること無くなんとなく過程だけ把握したい人向け)

djangoはPythonの MVC的フレームワーク (うるさい人はMVCではなくMTVと呼ぶ)

djangoではまず「プロジェクト」を作る。mysiteというアプリケーションは以下のように作れる

$ django-admin startproject mysite

すると、以下のようなディレクトリとファイル群が出来上がる

> ls -R mysite
mysite:
manage.py  mysite

mysite/mysite:
__init__.py  settings.py  urls.py  wsgi.py

以降の操作は基本的に mysite 配下で行うのが普通。

./manage.pyもしくはpython manage.pyからサブコマンドを発行して色々操作する。

> ./manage.py startapp polls
> ls -R
.:
manage.py  mysite  polls

./mysite:
__init__.py  __init__.pyc  settings.py  settings.pyc  urls.py  wsgi.py

./polls:
__init__.py  models.py  tests.py  views.py

以降 polls/ 配下の話。

実装は、Model (データ定義) を models.py を介して行うことから作業が始まる。SQLが暗黙のバックエンドとなる。

import datetime
from django.db import models
from django.utils import timezone

class Poll(models.Model):
  question = models.CharField(max_length=200)
  pub_date = models.DateTimeField('date published')

  def was_published_recently(self):
    now = timezone.now()
    return now - datetime.timedelta(days=1) <= self.pub_date < now

class Choice(models.Model):
  poll = models.ForeignKey(Poll)
  choice_text = models.CharField(max_length=200)
  votes = models.IntegerField(default=0)

利用できるフィールドについてはこちら

ここで、DBに書き込まれるであろうスキーマを見ることが出来る

> ./manage.py sql polls
BEGIN;
CREATE TABLE "polls_poll" (
    "id" integer NOT NULL PRIMARY KEY,
    "question" varchar(200) NOT NULL,
    "pub_date" datetime NOT NULL
)
;
CREATE TABLE "polls_choice" (
    "id" integer NOT NULL PRIMARY KEY,
    "poll_id" integer NOT NULL REFERENCES "polls_poll" ("id"),
    "choice_text" varchar(200) NOT NULL,
    "votes" integer NOT NULL
)
;
COMMIT;

注意: CREATE TABLE とあるが、このコマンド自体は実際にDBに書き込むわけではない。SQLステートメントを表示しているだけ。

syncdb で新たにデータベースを作成する。

> ./manage.py syncdb

Modelの変更に対して良きに計らう仕組みは django (as of 1.5) にはないので、DBをクリアするかALTER TABLE相当を自分で書くことになる。

開発中はクリアすることも多いと思う

> ./manage.py reset polls

south というフレームワークが別にあるので、試してみても良い。

DB構築後、それを読み込み表示するViewを作る。

(Viewの例は省略してる)

Modelの設計とView (とController)が分離していることで Unit Test を書きやすい。以下にチュートリアルから例を示す

def create_poll(question, days):
    return Poll.objects.create(question=question,
        pub_date=timezone.now() + datetime.timedelta(days=days))

class PollViewTests(TestCase):
    def test_index_view_with_a_past_poll(self):
        create_poll(question="Past poll.", days=-30)
        response = self.client.get(reverse('polls:index'))
        self.assertQuerysetEqual(
            response.context['latest_poll_list'],
            ['<Poll: Past poll.>']
        )

上記のテストでは、モックのDBに30日前のpollオブジェクトを書き込み、そのあと Indexを表示するView にアクセス、 その後、そのViewがデータベース内から書き込んだデータを適切に取得できていることを確認している。

参考: Tutorial 05 (Unit Test)

作っている最中は./manage.py runserverで仮のサーバを立てる。本番的に外部に公開するときには DEBUG を False にして動作するか確認して、 (djangoというよりPythonの仕組みである) wsgi 等でデプロイする。 (上記の例でも mysite/wsgi.pyというファイルがあるのが分かる)

ちなみにDEBUG=False はまず間違いなく最初はエラーを吐くのでご安心ください。

ここまで読んで満足したら本家のチュートリアルでもどうぞ。

Quick Start

* myprojoct * myapp

初期設定の変更

import os
BASE_DIR = os.path.dirname(os.path.dirname(__file__))

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': os.path.join(BASE_DIR, 'sqlite3.db'),
    }
}

タイムゾーンと言語を変更する

LANGUAGE_CODE = 'ja'
TIME_ZONE = 'Asia/Tokyo'

プロジェクト名と同名のアプリにHTMLのテンプレートや実装を全て置く。

(django 1.4のsettings.py を元にしている)

INSTALLED_APPS = (
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.sites',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    # Uncomment the next line to enable the admin:
    # 'django.contrib.admin',
    # Uncomment the next line to enable admin documentation:
    # 'django.contrib.admindocs',
    '(プロジェクト名)',                   # <------------- 足す。
)

データベースの生成

Sqlite3のDBをファイルシステム上に作る。

(mysiteディレクトリ)> ./manage.py syncdb
Creating tables ...
Creating table auth_permission
Creating table auth_group_permissions
Creating table auth_group
Creating table auth_user_user_permissions
Creating table auth_user_groups
Creating table auth_user
Creating table django_content_type
Creating table django_session
Creating table django_site

You just installed Django's auth system, which means you don't have any superusers defined.
Would you like to create one now? (yes/no): yes
Username (leave blank to use 'dmiyakawa'): 
E-mail address: dmiyakawa@mokha.co.jp
Password: 
Password (again): 
Superuser created successfully.
Installing custom SQL ...
Installing indexes ...
Installed 0 object(s) from 0 fixture(s)

Model とデータベースの作成

models.py をどこかのアプリケーションで作る。そのアプリを settings.py の INSTALLED_APPS に記述。

すると、 manage.py の sql コマンドでスキーマを表示出来るようになる。

mysite> ./manage.py sql mysite

DBへの書き込みはsyncdb

mysite> ./manage.py syncdb

Modelを変更する場合、手でSQLをいじるか、データベースをぶっ飛ばす。

./manage.py reset app_name

View の作成

注意: mysite/というプロジェクト名のディレクトリの配下にmysite/mysiteというアプリケーションディレクトリがある。 以下は全てプロジェクトのディレクトリを省略している。

mysite/urls.py を修正し、mysite/views.py を新規に作る。

from django.conf.urls import patterns, include, url

urlpatterns = patterns('',
    url(r'^$', 'mysite.views.home', name='home'),
)

HTMLのテンプレートを作る。 今回は mysite/templates/mysite/home.html を以下のように作成。

<html>
<head>
<title>Home</title>
<head>
<body>
  <h1>Home</h1>
  <p>message: {{ message }}</p>
</body>

次にmysite/views.pyを作成する。

# -*- coding=utf-8 -*-
from __future__ import print_function

import os.path
from os.path import dirname

from django.shortcuts import render

NAMESPACE = os.path.basename(dirname(dirname(os.path.abspath(__file__))))

def home(request):
    print('{}/home.html'.format(NAMESPACE))
    return render(request, '{}/home.html'.format(NAMESPACE),
                  {'message': 'hello'})

./manage.py runserver を実行してサーバを起動している間、http://127.0.0.1:8000/トップページを今用意したアプリケーションがWebブラウザから見られるはず。

Apache上で実行を確認する

(httpd.conf相当のファイルのどこかに)

WSGIDaemonProcess wsgi_user user=www-data group=www-data processes=1 threads=2 \
    maximum-requests=2 umask=0007 python-path=/opt/mysite

WSGIScriptAlias /mysite /opt/mysite/mysite/wsgi.py
<Directory /opt/mysite>
    Order deny,allow
    Allow from all
</Directory>

ログをファイルに保存する

(settings.pyへ)

LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'formatters': {
        'default': {
            'format': ('%(levelname)s %(asctime)s'
                       ' %(module)s %(lineno)s %(message)s'),
            'datefmt': '%Y-%m-%d %H:%M:%S'
        },
    },
    'handlers': {
        'debug': {
            'level': 'DEBUG',
            'class':'logging.handlers.RotatingFileHandler',
            'filename': os.path.join(LOG_DIR, 'debug.log'),
            'maxBytes': 5 * 1024 * 1024,
            'backupCount': 3,
            'formatter': 'default',
        },
    },
    'loggers': {
        'debug': {
            'handlers': ['debug'],
            'level': 'DEBUG',
            'propagate': False,
        },
    },
}

misc リンク

Django (last edited 2019-10-24 06:41:08 by DaisukeMiyakawa)