バックエンド編: Django REST frameworkとReact(TypeScript)でInstagramのクローンアプリを作成


事前知識

  • anaconda navigatorはPython開発のためのディストリビューション(コンパイル済みのソフトウェアの集まり)
  • jwtという認証トークンを使用する
  • djoserとはjwtトークンを簡単に使用するためのサードパーティー
  • django-cors-headersはReactからのアクセスを許可するために必要になってくる
  • Pillowは画像データを扱うために使用する

仮想環境を構築する

  1. Environments項目のCreateを選択
  2. Nameを入力し、Packages(Python 3.7)を選択
  3. 再生ボタンをクリックし、ターミナルを開く
  4. anaconda navigatorにて仮想環境(python3.7)の作成し、必要なものをインストール(下記コマンド)
$ pip install django==3.0.7
$ pip install djangorestframework==3.10
$ pip install djangorestframework-simplejwt==4.6.0
$ pip install djoser
$ pip install django-cors-headers
$ pip install Pillow

上記のコマンドにより各ライブラリ?のインストールが完了したら、anaconda navigatorとターミナルを閉じて、anaconda navigatorで作成した仮想環境とPycharmのプロジェクトに紐付ける

仮想環境とPycharmを紐付ける

  1. 空のディレクトリ(筆者はapi_insta)を作成し、Openを選択し、Pycharmで開く
  2. Preferences...を選択し、Projectの中のPython Interpreterを選択
  3. 右上の設定ボタン(歯車マーク)を選択し、Addを選択
  4. Existing envirionmentにチェックを入れ、右にある...を選択
  5. /Users/ホームディレクトリ/opt/anaconda3/envs/API_insta/bin/pythonを選択しOK

プロジェクトを作成する

$ django-admin startproject api_insta .

アプリを作成する

$ django-admin startapp api

現時点でのローカル環境を起動する

  1. manage.pyを右クリックし、Run 'manage'を選択
  2. 右上のmanageをクリックし、Edit Configulations...を選択
  3. Parametersという項目にrunserverを追加してOK
  4. 右上の再生ボタンをクリックで、ローカル環境を起動

モデルの作成

UserManagerではデフォルトではuser nameをpasswordで認証する様になっているのでemailとpasswordで認証するようにオーバーライドする必要がある

models.py
from django.db import models
from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin
from django.conf import settings

def upload_avatar_path(instance, filename):
    ext = filename.split('.')[-1]
    return '/'.join(['avatars', str(instance.userProfile.id)+str(instance.nickName)+str(".")+str(ext)])

def upload_post_path(instance, filename):
    ext = filename.split('.')[-1]
    return '/'.join(['posts', str(instance.userPost.id)+str(instance.title)+str(".")+str(ext)])

class UserManager(BaseUserManager):
    def create_user(self, email, password=None):
        if not email:
            raise ValueError('email is must')

        user = self.model(email=self.normalize_email(email))
        user.set_password(password)
        user.save(using=self._db)
        return user

    def create_superuser(self, email, password):
        user = self.create_user(email, password)
        user.is_staff = True
        user.is_superuser = True
        user.save(using= self._db)
        return user

class User(AbstractBaseUser, PermissionsMixin):
    email = models.EmailField(max_length=50, unique=True)
    is_active = models.BooleanField(default=True)
    is_staff = models.BooleanField(default=False)

    objects = UserManager()

    USERNAME_FIELD = 'email'

    def __str__(self):
        return self.email

class Profile(models.Model):
    nickName = models.CharField(max_length=20)
    userProfile = models.OneToOneField(
        settings.AUTH_USER_MODEL, related_name='userProfile',
        on_delete=models.CASCADE
    )
    created_on = models.DateTimeField(auto_now_add=True)
    img = models.ImageField(blank=True, null=True, upload_to=upload_avatar_path)

    def __str__(self):
        return self.nickName

class Post(models.Model):
    title = models.CharField(max_length=100)
    userPost = models.ForeignKey(
        settings.AUTH_USER_MODEL, related_name='userPost',
        on_delete=models.CASCADE
    )
    created_on = models.DateTimeField(auto_now_add=True)
    img = models.ImageField(blank=True, null=True, upload_to=upload_post_path)
    liked = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name='liked', blank=True)

    def __str__(self):
        return self.title

class Comment(models.Model):
    text = models.CharField(max_lengt=100)
    userComment = models.ForeignKey(
        settings.AUTH_USER_MODEL, related_name='userComment',
        on_delete=models.CASCADE
    )
    post = models.ForeignKey(Post, on_delete=models.CASCADE)

    def __str__(self):
        return self.text
  • normalze_email(email)はemailを正規化してuserをいう変数を作成している
  • set_password(password)はpasswordをハッシュ化している
  • データベースに保存
  • UserManagerをオーバーライドした場合はcreateSuperUserもオーバーライドする必要がある
  • Userモデルもemailバージョンにオーバーライドする

Active, Staff, Superuserの違いについて

Active: is_activeに指定する真偽値でアカウントの有効無効の切り替え(無効にするとログインできなくなる)
Staff: is_staggに指定する真偽値でstaff権限の有効無効の切り替え(staff権限はadminのダッシュボードにログインする権限)
Superuser: is_superuserに指定する真偽値で有効無効の切り替え(superusreはstaff権限に加え、データベースの内容の変更などの全権限を付与する)

settings.pyを編集

settings.py
from datetime import timedelta               #追記

INSTALLED_APPS = [
    #省略
    'rest_framework',                        #追記
    'djoser',                                #追記
    'api.apps.ApiConfig',                    #追記
    'corsheaders',                           #追記
]

MIDDLEWARE = [
    'corsheaders.middleware.CorsMiddleware', #追記
    #省略
]

#以下を追記
CORS_ORIGIN_WHITELIST = [
    "http://localhost:3000"
]

WSGI_APPLICATION = 'api_insta.wsgi.application'

#以下を追記
REST_FRAMEWORK = {
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.IsAuthenticated',
    ],
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework_simplejwt.authentication.JWTAuthentication',
    ],
}

#以下を追記
SIMPLE_JWT = {
    'AUTH_HEADER_TYPES': ('JWT',),
    'ACCESS_TOKEN_LIFETIME': timedelta(minutes=60),
}

TIME_ZONE = 'Asia/Tokyo'                     #UTCから変更
AUTH_USER_MODEL = 'api.User'                 #追記
MEDIA_ROOT = os.path.join(BASE_DIR, 'media') #追記
MEDIA_URL = '/media/'                        #追記
  • jwtトークンの有効期限を設定するためにtimedeltaを導入
  • INSTALLED_APPに導入したアプリを追加
  • djangoは.(ドット)で繋ぐことができるため、api.apps.ApiConfigとはapiディレクトリのapps.pyのApiConfigのことを指す
  • MIDDLEWAREにcorsに関するコードを追記
  • reactからのアクセスを許可するためにCORS_ORIGIN_WHITELISTを追加する
  • djangoのデフォルトの認証関連を設定する
  • views.pyを特定のユーザーだけに見せるようにするためにDAFAULT_PERMISSION_CLASSEを設定する
  • jwtを使用した認証をしたいため、DEFAULT_AUTHENTICATION_CLASSES
  • SIMPLE_JWTによってトークンの有効期限を60分に指定している
  • djangoにデフォルトのUserモデルではなく、カスタマイズしたUserモデルを使用するように明示的にする必要がある
  • プロジェクト直下にmediaディレクトリを作成し、画像の保存先を明示的にする(MEDIA_ROOTとMEDIA_URL)
  • migrateコマンドはデフォルトでsqlite3に変換してくれる

データベースに反映させる

$ python manage.py makemigrations
$ python manage.py migrate

ダッシュボードで追加したモデル内容を確認する

admin.py
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from django.utils.translation import gettext as _
from . import models

class UserAdmin(BaseUserAdmin):
    ordering = ['id']
    list_display = ['email']
    fieldsets = (
        (None, {'fields': ('email', 'password')}),
        (_('Personal Info'), {'fields': ()}),
        (
            _('Permissions'),
            {
                'fields': (
                    'is_active',
                    'is_staff',
                    'is_superuser',
                )
            }
        ),
        (_('Important dates'), {'fields': ('last_login',)}),
    )
    add_fieldsets = (
        (None, {
            'classes': ('wide',),
            'fields': ('email', 'password1', 'password2')
        }),
    )

admin.site.register(models.User, UserAdmin)
admin.site.register(models.Profile)
admin.site.register(models.Post)
admin.site.register(models.Comment)
  • オーバーライドせずに作成したモデル(PostやProfileモデルなど)はadmin.site.registerによってadmin dashboardで確認したい項目を追加できる
$ python manage.py createsuperuser
  • 管理者権限をもったユーザーの作成

シリアライザーの作成

シリアライザーとは

データベースからクライアント側にオブジェクトを渡す時にjsonに変換してくれたり、クライアント側からemailやpasswordなどをデータベースに渡す時に精査(validation)してくれたりするもの。
又、新規でモデルを作成した際にセットで作るもの。(User, Profile, Post, Commentの4つのシリアライザーを作成する)

  1. アプリケーションディレクトリ(startappコマンドで作成したディレクトリ)直下にserializers.pyを作成
serializers.py
from django.contrib.auth import get_user_model
from rest_framework import serializers
from .models import Profile, Post, Comment

class UserSerializer(serializers.ModelSerializer):
    class Meta:
        model = get_user_model()
        fields = ('id', 'email', 'password')
        extra_kwargs= {'password': {'write_only': True}}

    def create(self, validated_data):
        user = get_user_model().objects.create_user(**validated_data)
        return user