らっちゃいブログ

日々の学びと気づきを発信するブログ

DjangoでDBのフィールドに選択肢を設定する

概要

やりたいこととしては、特定のフィールドに指定できる値をEnum化するようなイメージです。

選択肢リストを設定する

フィールドに choices を指定することで選択肢を設定することができます。

choices の中身ですが『2要素のタプルからなる iterable(リストまたはタプル)』と定義されています。

つまりはこんな感じ。

GENDER_CHOICES = (
    (1, 'Male'),
    (2, 'Female'),
)
class Person(models.Model):
    gender = models.IntegerField(choices=GENDER_CHOICES)

見てわかる通り、一つ目の要素がDBに格納する値で二つ目が表示名です。

選択肢を利用するメリット/デメリット

メリットとしては、そのフィールドに格納され得るデータがプログラム上で定義できるため、追加変更が楽です。

また、以下のようにして設定値に対応する表示名を得ることができます。個人的にはこれがやりたくて使ってるところもあります。

person = Person(gender=1)
person.get_gender_display()
'Male'

デメリットですが、定義した選択肢の値が格納されることを強制できません。つまり設定した選択肢以外の値も保存され得ます。ですので、自分で入力チェックなりビジネスロジックを書いて意図しない値が入らないことを保証する必要があります。

MySQLENUM型でいいじゃん

という方もいそうなので、この話題にも言及しておきましょう。

ENUMを実際に使うとこんな感じでしょうか。

CREATE TABLE person (
  gender ENUM('Male', 'Female')
);

MySQLENUM型を使っても良いのですが、選択肢を増減するためにテーブル定義を変更する必要が出てくるため、運用を考えると使いたくないのです。また、DBに格納される値と同じEnum定数をプログラム上にも定義するケースが多く、定数の二重管理になりやすい点も無視できませんね。

ちなみにENUM型はSQLアンチパターン本でも扱われていますので、世間的にもよろしくないものと認知されていると思います。

SQLアンチパターン

SQLアンチパターン

SQLアンチパターンによると、ENUM型を使ってよいとされているのは、ON/OFFのように変更されることのない相互排他的な値を扱う場合だけとのことです。

今回例として挙げた性別はこれに該当するでしょうか?

いいえ、該当しません。依頼人の要望で『内緒』を保存したいということもあり得ます。もはや、ENUMは封印してしまうべきでしょう。

定数を管理するテーブルを作ればいいじゃん

ENUM を使わない場合は、以下のように参照テーブルを作る方法が一般的ですね。Django 以外を使うときは私もそうします。

CREATE TABLE gender (
  id VARCHAR(20),
  name VARCHAR(20)
);
INSERT INTO gender (key, name) VALUES (1, '男性'), (2, '女性');
 
CREATE TABLE person (
  gender_id INT(11),
  FOREIGN KEY (gender_id) REFERENCES gender(id) ON UPDATE CASCADE
);

確かにこれならやりたいことは実現できます。しかし、いつも思うのですが、表示名を取得するの面倒じゃないですか?わざわざ gender テーブルをジョインして name フィールドを引っ張ってくる必要がありますよね。それを避けるためには、結局プログラム上にIDと表示名を管理する Enum を定義することになり、定数の二重管理に逆戻りです。

結局、どうやってもプログラム上に Enum を定義することになるのですから、それだけで全部管理できるようにするのがベストだとは思いませんか?

本記事にて『入力チェックなりビジネスロジックを書いて意図しない値が入らないことを保証する必要がある』というデメリットをお伝えしましたが、そんなに難しいことではありませんよね。選択肢にない値を保存しようとしたらエラーにするだけで良いのですから、一か所に簡単なロジックを書くだけで済みます。

結論

ENUM型を使うのはやめてフィールドに選択肢を設定しよう!

Django REST frameworkでAPIにアクセス権を実装する方法

Django REST framework シリーズです。今回はアクセス権のお話をします。

Django REST framework を使ったことがないという方はこちらをどうぞ。

racchai.hatenablog.com

はじめに

API を実装する上で欠かせないのが、アクセス権の管理ですね。API 毎に実行可能かどうかをチェックする処理を入れるのは大変ですし、対応漏れも起きやすいです。かといって、自前で共通化するのも大変ですね。

Django REST framework では permission という概念でAPI毎にアクセス権を設定できるようになっていますので、本記事ではよくあるアクセス権を例に挙げて実装方法について紹介してみようと思います。

具体的な解説に入る前に、サンプルのAPIを作っておきます。

api.py

from rest_framework.generics import GenericAPIView
from rest_framework.response import Response

class SampleView(GenericAPIView):
    permission_classes = ()

    def get(self, request, format=None):
        return Response()

urls.py に以下を追加。

url(r'^sample', SampleView.as_view()),

SampleView に見慣れない permission_classes というフィールドが登場しましたが、Django REST framework ではここに Permission クラスを設定することでアクセス権を設定するようになります。

認証済みの場合のみアクセスさせたい

お決まりのやつですね。認証済みかどうかを検証するパーミッションについては、 Django REST Framework にて標準で用意されている IsAuthenticated がありますので、こちらを使うだけです。

from rest_framework.generics import GenericAPIView
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response

class SampleView(GenericAPIView):
    permission_classes = (IsAuthenticated,)

    def get(self, request, format=None):
        return Response()

permission_classes に IsAuthenticated を指定しました。これで認証済みでないと実行できないAPIとなります。

管理者のみアクセス可能

これもよくあるパターンですね。今回は特定のメールアドレス(admin@racchai.com)であれば管理者として判定するパーミッションを作成してみます。

作り方は簡単で、rest_framework.permissions.BasePermission を継承したクラスにて has_permission 関数を定義するだけです。has_permission 内でアクセス可能な条件を設定してあげれば、それだけでパーミッションクラスの完成となります。

from rest_framework.generics import GenericAPIView
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.permissions import BasePermission 

class IsAdmin(BasePermission):
    def has_permission(self, request, view):
        return request.user and request.user.email == 'admin@racchai.com'

class SampleView(GenericAPIView):
    permission_classes = (IsAdmin,)

    def get(self, request, format=None):
        return Response()

IsAdmin パーミッションを作成し、それを SampleView に設定しました。たったこれだけで、管理者しかアクセスできないAPIとなりました。

管理者のみ変更可能でそれ以外は閲覧のみさせたい

さきほど作成した IsAdmin パーミッションをさらに発展させてやるだけで実現できますね。新しいパーミッションIsAdminOrReadOnly として作成します。

from rest_framework.generics import GenericAPIView
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.permissions import BasePermission, SAFE_METHODS

class IsAdmin(BasePermission):
    def has_permission(self, request, view):
        return request.user and request.user.email == 'admin@racchai.com'

class IsAdminOrReadOnly(BasePermission):
    def has_permission(self, request, view):
        return request.method in SAFE_METHODS or IsAdmin().has_permission(request, view)

class SampleView(GenericAPIView):
    permission_classes = (IsAdminOrReadOnly,)

    def get(self, request, format=None):
        return Response()

    def post(self, request, format=None):
        return Response()

閲覧かどうかは、HTTP メソッドの種類で判別します。フレームワークの方で SAFE_METHODS(GET, HEAD, OPTIONS) が定義されているので、リクエストメソッドがそのどれかであれば True を返します。SAFE_METHODSでない場合は、IsAdmin パーミッションを評価することで実行可能かを判定するようになっています。

まとめ

簡単すぎてつまらない内容だったかもしれませんね。本当はパーミッションの仕組みでオブジェクトベースのアクセス権を設定することもできるのですが、長くなりそうなのでそれはまた別な機会にご紹介することにします。

それでは今回はこのへんで!

django-rest-framework-jwtの認証をカスタマイズする方法

django-rest-framework-jwt については以前こちらの記事でご紹介しました。

racchai.hatenablog.com

django-rest-framework-jwt では認証APIを標準で用意してくれているので、通常はそれを使うことになります。

ですが、論理削除されたユーザーの場合は認証を失敗させたりだとか、認証処理をカスタマイズしたい場合は自前で認証してアクセストークンを返す処理を用意する必要があります。

本記事では、企業コード/メールアドレス/パスワードにて認証するケースを想定したカスタマイズ方法についてご紹介します。

Django および Django REST framework が初めてという方はこちらをどうぞ!

racchai.hatenablog.com

racchai.hatenablog.com

では、さっそく始めます。

プロジェクトを作成する

さくっと作成していきます。

$ django-admin startproject racchai
$ cd racchai

settings.py にて INSTALLED_APPSDATABASES を編集。

INSTALLED_APPS = (
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
+   'rest_framework',
+   'rest_framework_jwt',
+   'racchai',
)

DATABASES = { 
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'racchai',
        'USER': 'root',
        'PASSWORD': 'root',
        'HOST': 'localhost',
    }   
}

ついでにMySQLにデータベースを作成しておきましょう。

$ mysql -uroot -p -e "CREATE DATABASE racchai";

はい、以上です。

認証モデルを定義する

Django のユーザーテーブルには企業コードは存在しませんので、追加しておく必要があります。

ユーザーテーブルのカス高い図については、ここでは詳しい解説は割愛します。よくわからないという方はこちらをご参照ください。

racchai.hatenablog.com

今回は models.py を以下のように作成するだけでひとまずはOKです。

from django.db import models    
from django.contrib.auth.models import AbstractBaseUser, BaseUserManager

class RacchaiUserManager(BaseUserManager):
    def create_user(self, email, password=None, **extra_fields):
        if not email:
            raise ValueError('Users must have a email address')

        email = RacchaiUserManager.normalize_email(email)
        user = self.model(email=email, **extra_fields)
        user.set_password(password)
        user.save(using=self._db)
        return user

    def create_superuser(self, email, password):
        return self.create_user(email, password)

class RacchaiUser(AbstractBaseUser):
    email = models.EmailField(max_length=128, unique=True)
    company = models.CharField(max_length=128, null=True)

    USERNAME_FIELD = 'email'

    objects = RacchaiUserManager()

    class Meta:
        db_table = 'racchai_user'
        swappable = 'AUTH_USER_MODEL'

本当は ./manage.py createsuperuser で企業コードも設定できるようにしたいところですが、今回はそこまではやりません。

RacchaiUserを作成したら、settings.py に以下を追記して認証モデルを差し替えておきます。

AUTH_USER_MODEL = 'racchai.RacchaiUser'

最後にマイグレーションを実行したら完了です。

$ ./manage.py migrate

念のためテーブル定義を確認。問題なさそうですね。

mysql> desc racchai_user;
+------------+--------------+------+-----+---------+----------------+
| Field      | Type         | Null | Key | Default | Extra          |
+------------+--------------+------+-----+---------+----------------+
| id         | int(11)      | NO   | PRI | NULL    | auto_increment |
| password   | varchar(128) | NO   |     | NULL    |                |
| last_login | datetime(6)  | YES  |     | NULL    |                |
| email      | varchar(128) | NO   | UNI | NULL    |                |
| company    | varchar(128) | NO   |     | NULL    |                |
+------------+--------------+------+-----+---------+----------------+

Serializerを定義する

次はAPIの入力情報を扱うSerializerを定義します。今回は入力値が3つあるので、以下のようになります。

jwt_auth.py

from rest_framework import serializers

class AuthInputSerializer(serializers.Serializer):
    company = serializers.CharField()
    email = serializers.EmailField()
    password = serializers.CharField()

View を定義する

最後に API 本体の実装です。さきほど Serializer を定義した jwt_auth.py に以下を追記しましょう。

from django.contrib.auth import authenticate
from rest_framework_jwt.settings import api_settings
from rest_framework.generics import GenericAPIView
from rest_framework.exceptions import AuthenticationFailed
from rest_framework.response import Response

jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER

class AuthView(GenericAPIView):
    serializer_class = AuthInputSerializer
    def post(self, request):
        serializer = AuthInputSerializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        user = authenticate(**serializer.data)
        if not user:
            raise AuthenticationFailed()
        # 以下2行が今回のカスタマイズ部分
        if user.company != serializer.data['company']:
            raise AuthenticationFailed()
        payload = jwt_payload_handler(user)
        return Response({
            'token': jwt_encode_handler(payload),
        })

最後に urls.py に以下を追記して /jwt_auth でアクセスできるようにします。

from racchai.jwt_auth import AuthView

urlpatterns = [
    url(r'^jwt_auth', AuthView.as_view()),
]

これで認証ができるようになったはずです。

試してみる

API を試す前に、まだユーザーを登録していなかったので先にユーザーを作成しておきます。

$ ./manage.py createsuperuser
Email: jwt@racchai.com
Password: jwt
Password (again): jwt
Superuser created successfully.

前述の通り、この時点ではユーザーレコードに会社コードが設定されていませんので、SQL で直接会社コードを設定してしまいます。会社コードは仮に0001としておきます。

$  mysql -uroot -p racchai -e "update racchai_user set company = '0001'"

準備が整ったところで、開発サーバーを起動して認証APIを実行してみます。

$ ./manage.py runserver
....

$ curl http://localhost:8000/jwt_auth -d "email=jwt@racchai.com&password=jwt&company=0001"
{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Imp3dEByYWNjaGFpLmNvbSIsInVzZXJfaWQiOjIsImVtYWlsIjoiand0QHJhY2NoYWkuY29tIiwiZXhwIjoxNDYzNjI1NDQ5fQ.YX6a_9680xDDYOiqwXX1kAZA1dMH3rLIXq7oGTaOawc"}

無事JWTアクセストークンを取得できましたね!

念のため、会社コードを間違えたらどうなるのか見ておくことにします。

$ curl http://localhost:8888/jwt_auth -d "email=jwt@racchai.com&password=jwt&company=0002"
{"detail":"Incorrect authentication credentials."}

ちゃんと認証に失敗することも確認できましたね。

まとめ

だいぶ早足でしたが、カスタマイズ自体は難しいことではないってことだけ伝われば幸いです!

DjangoでJWTを使ったトークン認証を実装する

JWT とは

Json Web Token の略。一言でいってしまうとJSONに署名したもの。こちらの記事で詳しく解説されています。

JSON Web Token の効用 - Qiita

はじめに

さっそく環境を作っていきます。最初はお決まりのプロジェクト作成です。

詳しい手順と説明はこちらの記事をご参照ください。

racchai.hatenablog.com

Lightweight Django

Lightweight Django

ではプロジェクトを作成します。

$ django-admin startproject racchai
$ cd racchai

今回は認証を扱うので、データベースとユーザーも作成しておきます。

$ ./manage.py migrate                                                                                                                                  │
Operations to perform:                                                                                                                                                                     │
  Synchronize unmigrated apps: staticfiles, messages                                                                                                                                       │
  Apply all migrations: admin, contenttypes, auth, sessions                                                                                                                                │
Synchronizing apps without migrations:                                                                                                                                                     │
  Creating tables...                                                                                                                                                                       │
    Running deferred SQL...                                                                                                                                                                │
  Installing custom SQL...                                                                                                                                                                 │
Running migrations:                                                                                                                                                                        │
  Rendering model states... DONE                                                                                                                                                           │
  Applying contenttypes.0001_initial... OK                                                                                                                                                 │
  Applying auth.0001_initial... OK                                                                                                                                                         │
  Applying admin.0001_initial... OK                                                                                                                                                        │
  Applying contenttypes.0002_remove_content_type_name... OK                                                                                                                                │
  Applying auth.0002_alter_permission_name_max_length... OK                                                                                                                                │
  Applying auth.0003_alter_user_email_max_length... OK                                                                                                                                     │
  Applying auth.0004_alter_user_username_opts... OK                                                                                                                                        │
  Applying auth.0005_alter_user_last_login_null... OK                                                                                                                                      │
  Applying auth.0006_require_contenttypes_0002... OK                                                                                                                                       │
  Applying sessions.0001_initial... OK

$ ./manage.py createsuperuser
Username (leave blank to use 'kon'): racchai
Email address: jwt@racchai.com
Password: racchai
Password (again): racchai
Superuser created successfully.

何度やっても楽ちんですね。

トークンを取得してみる

今回は django-rest-framework-jwt を使うのでインストールしておきます。

$ pip install djangorestframework-jwt

インストールができたらracchai/settings.py に以下を追記します。

REST_FRAMEWORK = { 
    'DEFAULT_PERMISSION_CLASSES': (
        'rest_framework.permissions.IsAuthenticated',
    ),  
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
    ),  
    'NON_FIELD_ERRORS_KEY': 'detail',
    'TEST_REQUEST_DEFAULT_FORMAT': 'json'
}

お次は racchai/urls.py を以下のように編集。

+ from rest_framework_jwt.views import obtain_jwt_token

urlpatterns = [
+ url(r'^jwt-token', obtain_jwt_token),
]

基本設定は以上です。あっという間ですね。では開発サーバーを起動してトークンを取得してみましょう。

$ ./manage.py runserver
$ curl http://localhost:8000/jwt-token -d "username=racchai&password=racchai"
{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InJhY2NoYWkiLCJ1c2VyX2lkIjoxLCJlbWFpbCI6Imp3dEByYWNjaGFpLmNvbSIsImV4cCI6MTQ2MTY1MzY0N30.UBZrYz79xeI25-Uufmh2zdwZXXzHjamh9rHeOS2Fuqk"}

無事取得できました。簡単すぎて本当に動いてるのか疑いたくなりますね。念のためパスワードを間違えてみましょう。

$ curl http://localhost:8000/jwt-token -d "username=racchai&password=jwt"
{"detail":["Unable to login with provided credentials."]}

ちゃんと認証失敗しました。安心してください、動いてます。

トークンを使って認証が必要なAPIを実行してみる

まずは認証が必要なAPIを用意します。django-rest-framework-jwt を使っているのですから、APIdjango-rest-framework で作ることにします。

django-rest-framework の初期設定についてはこちらの記事で紹介しています。

racchai.hatenablog.com

次は、ログインユーザー名で返事するだけのAPIを作成しましょう。

racchai/views.py

from rest_framework.generics import GenericAPIView
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework import status

class PingViewSet(GenericAPIView):
    permission_classes = (IsAuthenticated,)

    def get(self, request, format=None):
        return Response(data={'username': request.user.username}, status=status.HTTP_200_OK)

APIが作成できたら racchai/urls.py を以下のように編集します。

from django.conf.urls import url
from rest_framework_jwt.views import obtain_jwt_token
+ from racchai import views

urlpatterns = [
    url(r'^jwt-token', obtain_jwt_token),
+   url(r'^ping', views.PingViewSet.as_view()),
]

準備ができたら作成したAPIにアクセスしてみましょう。

$ curl http://localhost:8000/ping
{"detail":"Authentication credentials were not provided."}

認証情報が指定されてないと怒られてしまいました。では JWT トークンをリクエストに乗せて再度トライ。

$ curl http://localhost:8888/ping  -H "Authorization: JWT eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InJhY2NoYWkiLCJ1c2VyX2lkIjoxLCJlbWFpbCI6Imp3dEByYWNjaGFpLmNvbSIsImV4cCI6MTQ2MTY1MzY0N30.UBZrYz79xeI25-Uufmh2zdwZXXzHjamh9rHeOS2Fuqk"
{"username":"racchai"}

無事認証が通ってログインユーザーの情報を取得できていますね!

まとめ

django-rest-framework-jwt を使うことで、一瞬でトークンベースの認証を実現することができました。携帯アプリと連携する際には重宝する機能だと思いますので、ぜひご活用ください。

Pythonプロフェッショナルプログラミング 第2版

Pythonプロフェッショナルプログラミング 第2版

Djangoを使ったメールの送信方法まとめ

Python の smtplib モジュール に慣れている人には不要かもしれませんが、Django にもメール送信するための仕組みが備わっていますので簡単に紹介してみます。

Django 自体使ったことがないという方はこちらをどうぞ。

racchai.hatenablog.com

はじめに

本記事はこちらのコマンドでダミーSMTPサーバを起動している前提で話を進めます。

$ sudo python -m smtpd -n -c DebuggingServer -d localhost:25

settings.py に上記のサーバーへのアクセス情報を記載しておきましょう。これだけで DjangoSMTPサーバを認識してくれます。

EMAIL_HOST = 'localhost'
EMAIL_PORT = 25

なお、AWSの SES 等を利用する場合は当然認証情報が必要ですので、以下の項目を追加しておいてください。

EMAIL_HOST_USER = ユーザー
EMAIL_HOST_PASSWORD = パスワード
EMAIL_USE_TLS = True

単純なメール送信

それでは Djangoインタラクティブコンソールでメールを送ってみます。使うのは一番単純なメール送信関数である send_mail です。

$ ./manage.py shell
>>> from django.core.mail import send_mail
>>>send_mail('hello', 'this is message', 'from@racchai.com', ['to@racchai.com'])
1

返り値は送信に成功した数なので、今回は1が返れば成功です。起動しておいたダミーSMTPサーバのコンソールを確認してみます。

MIME-Version: 1.0
Content-Type: text/plain; charset="utf-8"
Content-Transfer-Encoding: 7bit
Subject: hello
From: from@racchai.com
To: to@racchai.com
Date: Wed, 27 Apr 2016 08:54:50 -0000
Message-ID: <20160427085450.80900.67436@ubuntu.local>
X-Peer: 127.0.0.1

this is message

無事メールが送れていますね。

複数のメールを一度に送信する

メール本文に名前を入れる等の理由で送信先毎に内容が異なるような場合には send_mass_mail を使います。

$ ./manage.py shell
>>> from django.core.mail import send_mass_mail
>>> message1 = ('message1 subject', 'this is message1', 'from@racchai.com', ['to1@racchai.com'])
>>> message2 = ('message2 subject', 'this is message2', 'from@racchai.com', ['to2@racchai.com'])
>>> send_mass_mail((message1, message2))
2

2通のメール送信に成功しました。

---------- MESSAGE FOLLOWS ----------
MIME-Version: 1.0
Content-Type: text/plain; charset="utf-8"
Content-Transfer-Encoding: 7bit
Subject: message1 subject
From: from@racchai.com
To: to1@racchai.com
Date: Wed, 27 Apr 2016 09:04:22 -0000
Message-ID: <20160427090422.80900.33900@ubuntu.local>
X-Peer: 127.0.0.1

this is message1
------------ END MESSAGE ------------

---------- MESSAGE FOLLOWS ----------
MIME-Version: 1.0
Content-Type: text/plain; charset="utf-8"
Content-Transfer-Encoding: 7bit
Subject: message2 subject
From: from@racchai.com
To: to2@racchai.com
Date: Wed, 27 Apr 2016 09:04:22 -0000
Message-ID: <20160427090422.80900.62258@ubuntu.local>
X-Peer: 127.0.0.1

this is message2
------------ END MESSAGE ------------

SMTP サーバの方にも確かに2通分届いていますね。

ちなみに send_mass_mail では複数のメールを送る場合でも一度しかSMTPサーバとの接続を張らないようになっているため、大量の接続が張られてしまう心配はありません。

メールにファイルを添付する

send_mailsend_mass_mail にはファイルを添付する機能がありません。シンプルにテキストメールを送ることしかできないのです。そのため、ファイル添付のようなことをやりたくなったら、ある程度は自分で処理を書く必要があります。とはいっても大した処理ではありません。

添付ファイルを送る処理はこのようになります。

>>> from django.core.mail import EmailMessage
>>> message = EmailMessage('subject', 'this is message', from_email='from@racchai.com', to=['to@racchai.com'])
>>> message.attach('sample.txt', 'contents', 'text/plain')
>>> message.send()
1

では実際に送信されるメールを見てみます。

---------- MESSAGE FOLLOWS ----------
Content-Type: multipart/mixed; boundary="===============5970738729801732969=="
MIME-Version: 1.0
Subject: subject
From: from@racchai.com
To: to@racchai.com
Date: Wed, 27 Apr 2016 12:28:09 -0000
Message-ID: <20160427122809.80900.57799@ubuntu.local>
X-Peer: 127.0.0.1

--===============5970738729801732969==
MIME-Version: 1.0
Content-Type: text/plain; charset="utf-8"
Content-Transfer-Encoding: 7bit

this is message
--===============5970738729801732969==
MIME-Version: 1.0
Content-Type: text/plain; charset="utf-8"
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment; filename="sample.txt"

contents
--===============5970738729801732969==--
------------ END MESSAGE ------------

multipart なメッセージになり、sample.txt が添付されていますね。

ちなみに、CC や BCC も EmailMessage を使うことで実現できます。以下のように cc と bcc を指定してあげればOKです。

EmailMessage('subject', 'this is message', from_email='from@racchai.com', to=['to@racchai.com'], cc=['cc@racchai.com'], bcc=['bcc@racchai.com'])

複数の EmailMessageを一度に送信する

send_mass_mail の中で、複数のメールを送る際に一度しかSMTPサーバとの接続を張らないようになっているという話をしました。以下のようにすることで、send_mass_mail と同様に複数のEmailMessage を効率的に送信することができるようになります。

>>> from django.core import mail
>>> connection = mail.get_connection()
>>> message1 = mail.EmailMessage('subject', 'this is message1', from_email='from@racchai.com', to=['to1@racchai.com'])
>>> message2 = mail.EmailMessage('subject', 'this is message2', from_email='from@racchai.com', to=['to2@racchai.com'])
>>> connection.send_messages([message1, message2])
2
>>> connection.close()

connection を手動で張っているため、クローズするのを忘れないように注意しましょう。

ひとこと

Django でのメール送信には多少のコードを書く必要がありますが、それでも smtplib モジュールを使うよりは楽に送信することができますね。それではまた!

Django×Python (LLフレームワークBOOKS)

Django×Python (LLフレームワークBOOKS)

Djangoで認証ユーザーモデルをカスタマイズする

Djangoで認証を行ったりした際に得られる User オブジェクトですが、デフォルトのままだと不要なデータが多かったり、必要なフィールドがなかったりします。

そんなときのために、今回はデフォルトのモデルである django.contrib.auth.models.Userをカスタマイズして、新しい User モデルを作成する方法について解説します。

プロジェクトを作成

お決まりの racchai プロジェクトを作成します。

$ django-admin startproject racchai

django のインストールがまだという方はこちらを参考にインストールしておきましょう。

racchai.hatenablog.com

プロジェクトが作成できたら、racchai プロジェクトをINSTALLED_APPS に追加しておきます。

# Application definition

INSTALLED_APPS = (
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
+   'racchai',
)

プロジェクトの作成はこれでおしまいです。

データベースの設定

racchai/settings.pyを見てみましょう。

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

初期設定では sqliteを使うことになっていますね。本記事では MySQL を使って進めていきますので、MySQL を使うように設定を変更しておきます。

mysql のユーザー名およびパスワードについては自身の環境にあわせて置き換えてください。

DATABASES = { 
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'racchai',
        'USER': 'root',
        'PASSWORD': 'root',
        'HOST': 'localhost',
    }   
}

上記の設定で、ローカルにインストールされた MySQL の racchai というデータベースを参照するようになりました。

さっそくデータベースとテーブルを作成してみましょう。

っとその前に、まだ python から MySQL へアクセスするためのドライバをインストールしていませんので、インストールしておく必要があります。

$ pip install MySQL-python

これで準備が整いましたので、データベースとテーブルを作成します。

$ mysql -uroot -p -e "CREATE DATABASE racchai";
$ ./manage.py migrate
Operations to perform:
  Synchronize unmigrated apps: staticfiles, messages
  Apply all migrations: admin, contenttypes, auth, sessions
Synchronizing apps without migrations:
  Creating tables...
    Running deferred SQL...
  Installing custom SQL...
Running migrations:
  Rendering model states... DONE
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying sessions.0001_initial... OK

はい、作成されました。

ではユーザーモデルに対応するテーブル情報を見てみましょう。

$ mysql -uroot -p racchai
mysql> desc auth_user;
+--------------+--------------+------+-----+---------+----------------+
| Field        | Type         | Null | Key | Default | Extra          |
+--------------+--------------+------+-----+---------+----------------+
| id           | int(11)      | NO   | PRI | NULL    | auto_increment |
| password     | varchar(128) | NO   |     | NULL    |                |
| last_login   | datetime(6)  | YES  |     | NULL    |                |
| is_superuser | tinyint(1)   | NO   |     | NULL    |                |
| username     | varchar(30)  | NO   | UNI | NULL    |                |
| first_name   | varchar(30)  | NO   |     | NULL    |                |
| last_name    | varchar(30)  | NO   |     | NULL    |                |
| email        | varchar(254) | NO   |     | NULL    |                |
| is_staff     | tinyint(1)   | NO   |     | NULL    |                |
| is_active    | tinyint(1)   | NO   |     | NULL    |                |
| date_joined  | datetime(6)  | NO   |     | NULL    |                |
+--------------+--------------+------+-----+---------+----------------+

いろいろ定義されていますね。次はいよいよカスタマイズする方法です。

RacchaiUserモデルを作成する

今回は以下のようなモデルを作成してみます。

  • テーブル名を racchai_user とする
  • ログイン名として email フィールドを持つ
  • twitter の URL を持つ

ではやってみます。

racchai/models.py というファイルを作り、その中で RacchaiUser クラスを定義しましょう。

from django.db import models
from django.contrib.auth.models import AbstractBaseUser, BaseUserManager

class RacchaiUserManager(BaseUserManager):
    def create_user(self, email, password=None, **extra_fields):
        if not email:
            raise ValueError('Users must have a email address')
        email = RacchaiUserManager.normalize_email(email)
        user = self.model(email=email, **extra_fields)
        user.set_password(password)
        user.save(using=self._db)
        return user

    def create_superuser(self, email, password):
        return self.create_user(email, password)

class RacchaiUser(AbstractBaseUser):
    email = models.EmailField(max_length=128, unique=True)
    twitter_url = models.URLField()

    USERNAME_FIELD = 'email'

    objects = RacchaiUserManager()

    class Meta:
        db_table = 'racchai_user'
        swappable = 'AUTH_USER_MODEL'

RacchaiUser を作成すると同時に RacchaiUserManager も作成していますが、ユーザーモデルをカスタマイズする際はこれらをセットで作成する必要があるということだけ覚えておいてもらえれば大丈夫です。

次は、Django が参照しているモデルクラスを差し替えます。racchai/settings.py にて以下を追記してください。

AUTH_USER_MODEL = 'racchai.RacchaiUser'

できましたか?

たったこれだけでカスタマイズしたUserモデルに差し替えることができています。マイグレーションして確認してみましょう。

$ ./manage.py makemigrations racchai
Migrations for 'racchai':
  0001_initial.py:
    - Create model RacchaiUser

$ ./manage.py migrate
Operations to perform:
  Synchronize unmigrated apps: staticfiles, messages
  Apply all migrations: admin, contenttypes, sessions, auth, racchai
Synchronizing apps without migrations:
  Creating tables...
    Running deferred SQL...
  Installing custom SQL...
Running migrations:
  Rendering model states... DONE
  Applying racchai.0001_initial... OK
The following content types are stale and need to be deleted:

    auth | user

Any objects related to these content types by a foreign key will also
be deleted. Are you sure you want to delete these content types?
If you're unsure, answer 'no'.

    Type 'yes' to continue, or 'no' to cancel: yes

マイグレーションは無事成功です。次は肝心のテーブル情報を見てみましょう。

$ mysql -uroot -p racchai
mysql> desc racchai_user;
+-------------+--------------+------+-----+---------+----------------+
| Field       | Type         | Null | Key | Default | Extra          |
+-------------+--------------+------+-----+---------+----------------+
| id          | int(11)      | NO   | PRI | NULL    | auto_increment |
| password    | varchar(128) | NO   |     | NULL    |                |
| last_login  | datetime(6)  | YES  |     | NULL    |                |
| email       | varchar(128) | NO   | UNI | NULL    |                |
| twitter_url | varchar(200) | NO   |     | NULL    |                |
+-------------+--------------+------+-----+---------+----------------+

racchai_user テーブルが作成されており、中身はさきほど RacchaiUser クラスで定義したものになっていることがわかりますね。

動作確認

では実際にDjangoが参照するユーザーモデルが差し替わっているのか確認してみます。

ユーザー作成

createsuperuser コマンドで racchai_user テーブルにデータが登録されるかを確認してみます。

$ ./manage.py createsuperuser
Email: test@racchai.com
Password: test
Password (again): test
Superuser created successfully.

$ mysql -uroot -p racchai -e "SELECT * FROM racchai_user"
+----+-------------------------------------------------------------------------------+------------+---------------+-------------+
| id | password                                                                      | last_login | email         | twitter_url |
+----+-------------------------------------------------------------------------------+------------+---------------+-------------+
|  1 | pbkdf2_sha256$20000$MlRFZRiDro4P$lIhUqpljqcqWDiaj0AnCTB8Y2NWBE5wempF31hY6jws= | NULL       | test@racchai.com |             |
+----+-------------------------------------------------------------------------------+------------+---------------+-------------+

入りました!

認証

ユーザー認証の結果として得られるユーザーオブジェクトが差し替わっているかを確認します。

$ ./manage.py shell
>>> from django.contrib.auth import authenticate
>>> authenticate(email='test@racchai.com', password='test')
<RacchaiUser: test@racchai.com>

無事認証結果が RacchaiUser オブジェクトになってますね!

まとめ

今回はユーザーモデルをカスタマイズする方法についてご紹介してみました。

多少手間ではありますが、サービスに合ったユーザーモデルにカスタマイズするだけで開発効率が全然違ってくると思いますので、ぜひ試してみてください。

Django REST framework でバリデーションしてみよう

みなさんお待ちかねの Django REST framework シリーズです。

バリデーションって大事なんだけど仕組みを作るのって地味にめんどくさいですよね。

Django REST framework ではそこそこ簡単に書けるようになってますので、ご紹介します。

事前準備

まずは簡単にプロジェクトを作成します。

詳しい手順と説明はこちらの記事をご参照ください。

racchai.hatenablog.com

ではプロジェクトを作成します。

$ django-admin startproject racchai
$ cd racchai

Serializer クラスを作成する

前回の記事でも触れましたが、バリデーションは Serializer を使用して実現します。

簡単な Serializer を用意しましたので、作成しましょう。

$ cat > racchai/serializers.py <<EOF
from rest_framework import serializers

class SimpleValidationSerializer(serializers.Serializer):
    integer= serializers.IntegerField()
EOF

これだけで、string フィールドは必須で、文字列のみというバリデーションができるSerializerが作成できました。

検証してみる

インタラクティブコンソールで検証してみましょう。

$ ./manage.py shell
>>> from racchai.serializers import SimpleValidationSerializer
>>> s = SimpleValidationSerializer(data={'integer': 1})
>>> s.is_valid()
True

string に文字列を指定すると、もちろん True となります。

次は 空データで試してみましょう。

>>> s = SimpleValidationSerializer(data={})
>>> s.is_valid()
False

今度は失敗しました。バリデーションが実行されていそうですね。

バリデーションメッセージを取得する

バリデーションに失敗したら、バリデーションメッセージを取得してみましょう。

>>> s = SimpleValidationSerializer(data={})
>>> s.is_valid()
False
>>> s.errors
{'integer': [u'This field is required.']}

integer フィールドが指定されていないことが原因だということがわかりますね。

試しに文字列を指定した場合のメッセージも見てみましょう。

>>> s = SimpleValidationSerializer(data={'integer': 'str'})
>>> s.is_valid()
False
>>> s.errors
{'integer': [u'A valid integer is required.']}

ちゃんと数値でない値を指定したことが原因であることがわかります。

バリデーションメッセージをカスタマイズする

バリデーションメッセージは自前で用意することが多いですよね。

今回はシンプルに Serializer 内でメッセージを差し替える方法を紹介します。

SimpleValidationSerializer を以下のように変更してみましょう。

class SimpleValidationSerializer(serializers.Serializer):
    integer= serializers.IntegerField(error_messages={'required': 'racchai!'})

ではバリデーションエラーを表示してみます。

>>> from racchai.serializers import SimpleValidationSerializer
>>> s = SimpleValidationSerializer(data={})
>>> s.is_valid()
False
>>> s.errors
{'integer': ['racchai!']}

racchai!

成功です。

意図しないデータをフィルタリングする

バリデーションを通過したら、その結果のデータを取得することになります。

その際、Serializer は自身に定義されていないパラメータを除去してくれるようになっています。

試しに、不要なデータをバリデーションにかけてみます。

>>> s = SimpleValidationSerializer(data={'integer': 1, 'not_defined': 1})
>>> s.is_valid()
True
>>> s.validated_data
OrderedDict([(u'integer', 1)])

ちゃんと not_defined が除去されて、Serializer で定義されたフィールドだけが取得できましたね。

ちなみに、validated_datais_valid を呼び出した後でないとアクセスできません。

でないと以下のようなエラーになります。

AssertionError: You must call `.is_valid()` before accessing `.validated_data`.

エラーメッセージを読めばすぐ原因がわかるものではありますが、このエラーを見て困惑する人は多い気はしますね。

おまけ

is_valid 関数には、raise_exception=True を与えることで erorrs 情報をラップした ValidationError を投げてくれる機能がついています。

>>> s = SimpleValidationSerializer(data={'integer': 'str'})
>>> s.is_valid(raise_exception=True)
ValidationError: {'integer': [u'A valid integer is required.']}

自分で API を実装するときは、楽なのでこの方法を使うことにしています。

最後に

いかがでしたでしょうか。

シンプルな設計なので、わかってしまえば直観的にサクサク実装を進められそうですね。

今回はカスタムバリデーターを作成する方法については触れませんでしたが、近いうちにご紹介できればと思います。

それではまた!