バックエンド編: 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
```
- get_user_modelはdjangoの標準の関数でアクティブなUserモデルを取得できる
#tips
- gitignoreに関して(https://note.com/masato1230/n/na63ac4e7ccdd)