Docker+Django+Next+TypeScript+ECSでアプリを作った話(2) ~ Djangoの初期設定からModel作成編  ~

# はじめに

前回の続きです。

前の記事: [Docker+Django+Next+TypeScript+ECSでアプリを作った話(1) ~ 準備編 ~](https://qiita.com/rys0707/items/ebe031c694c30831dc47)
次の記事: [Docker+Django+Next+TypeScript+ECSでアプリを作った話(3) ~ Djangoのスキーマ作成からデータ取得まで~](https://qiita.com/rys0707/items/94263ed21f489fb21aae)

今回はDjangoプロジェクトの初期設定から、Model作成までを書きました。　　
バックエンドとしてDjangoを採用しましたが、DjangoをAPI化するのにgraphene-djangoを採用しました。　　
graphene-djangoに関しては以下の記事を参考にさせていただきました。

https://qiita.com/hharu/items/ebe29501c0ab5f832911

# 1.初期設定

開発・テスト環境用と本番環境用のsettingsを作成します。また、本番環境用にSECRET_KEYを管理する.envを作成し、django-environというパッケージにて管理します。

```diff
myProject/
　　　 app/
+   .env
　　　　　　　　app/
        settings.py
+　　　　　　    dev_settings.py
+　　　　　　    prod_settings.py
        ...
```

- settings.py　　

全ての開発環境で共通する部分を記載しています。SECRET_KEYやDEBUGは環境別settingsに記載しています。
ALLOWED_HOSTSは環境ごとに変更できるように、環境変数を入れています。
その他に、graphene-djangoとDBの設定の変更や、言語設定を日本語に、タイムゾーンを日本に変更しています。
また、AUTH_USER_MODELを後に作成するUserモデルに設定しています。

```diff:settings.py
+ import os
  from pathlib import Path

  BASE_DIR = Path(__file__).resolve().parent.parent
-
- SECRET_KEY = '1234567890**************************'
- DEBUG = True 
- 
- ALLOWED_HOSTS = []
+ ALLOWED_HOSTS = [os.environ['ALLOWED_HOST']]
 
  INSTALLED_APPS = [
      'django.contrib.admin',
      'django.contrib.auth',
      'django.contrib.contenttypes',
      'django.contrib.sessions',
      'django.contrib.messages',
      'django.contrib.staticfiles',
+     'graphene_django',
+     'corsheaders',
+     'api.apps.ApiConfig',
  ]

  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',
+     'corsheaders.middleware.CorsMiddleware',
  ]

+
+ CORS_ORIGIN_WHITELIST = [
+     os.environ['FRONT_URI']
+ ]
+
+ GRAPHWL_JWT = {
+     'JWT_VERIFY_EXPORATION': False,
+ }
+
+ GRAPHENE = {
+     'SCHEMA':
+         'app.schema.schema',
+         'MIDDLEWARE': [
+             'graphql_jwt.middleware.JSONWebTokenMiddleware',
+         ],
+ }
+ 
+ AUTHENTICATION_BACKENDS = [
+     'graphql_jwt.backends.JSONWebTokenBackend',
+     'django.contrib.auth.backends.ModelBackend',
+ ]

  ROOT_URLCONF = 'app.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 = 'app.wsgi.application'

  DATABASES = {
      'default': {
-          'ENGINE': 'django.db.backends.sqlite3',
+          'ENGINE': 'django.db.backends.postgresql',
-          'NAME': BASE_DIR / 'db.sqlite3',
+          'NAME': os.environ['DB_NAME'],
+          'USER': os.environ['DB_USER'],
+          'HOST': os.environ['DB_HOST'],
+          'PORT': 5432,
       }
  }

  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',
      },
  ]

- LANGUAGE_CODE = 'en-us'
+ LANGUAGE_CODE = 'ja'
-
- TIME_ZONE = 'UTC'
+ TIME_ZONE = 'Asia/Tokyo'

  USE_I18N = True

  USE_L10N = True

  USE_TZ = True
+
+ AUTH_USER_MODEL = 'api.CustomUser'

  STATIC_URL = '/static/'
+
+ STATIC_ROOT = os.path.join(BASE_DIR, 'static')
```

- dev_settings.py  

開発環境、テスト環境用のsettingsを記載しています。
SECRET_KEYは本番環境とは異なる値にしています。

```py:dev_settings.py
from .settings import *

DEBUG = True

SECRET_KEY = '1234567890**************************'
```

- prod_settings.py  

本番環境用のsettingsを記載しています。
SECRET_KEYはdjango-environにて.envに記載しています。

```py:dev_settings.py
import os
import environ

from .settings import *

env = environ.Env()
env.read_env(os.path.join(BASE_DIR, '.env'))

DEBUG = False

SECRET_KEY = env('SECRET_KEY')
```

- .env　　
本番環境用のSECRET_KEYを記載しています。 

```txt:.env
SECRET_KEY=abcdefghijk************************
```

# 2.Model作成

UserモデルとProfileモデルを作成します。
apiフォルダ直下のmodels.pyに記載します。

- models.py  
UserモデルはDjango標準のAbstractBaseUserをオーバーライドして作成します。<br/>
email、passwordでの認証にするため、name属性はProfileモデルに分け、Userモデルは必要最低限の属性にしています。

```py:models.py

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

class CustomUserManager(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 CustomUser(AbstractBaseUser, PermissionsMixin):
    email = models.EmailField(max_length=50, unique=True)
    is_active = models.BooleanField(default=True)
    is_staff = models.BooleanField(default=False)

    objects = CustomUserManager()

    USERNAME_FIELD = 'email'

    def __str__(self):
        return self.email

class Profile(models.Model):
    nickname = models.CharField(max_length=20)
    user = models.OneToOneField(
        CustomUser, related_name="profile",
        on_delete=models.CASCADE
    )

    def __str__(self):
        return self.nickname

```

- admin.py  
Django標準の管理画面にてUserモデルとProfileモデルをCRUD操作できるように記載します。

```py:admin.py
from django.contrib import admin
from .models import CustomUser, Profile

admin.site.register(CustomUser)
admin.site.register(Profile)

```

- docker-compose.yml
Djangoコンテナ起動時に、開発環境用のsettingsを使用するように修正します。

```diff:docker-compose.yml
  version: "3"

  services:
    app:
      build:
        context: .
      ports:
        - "8000:8000"
      volumes:
        - ./app:/app
-      command: sh -c "python manage.py migrate &&
-       python manage.py runserver 0.0.0.0:8000"
+      command: sh -c "python manage.py migrate --settings app.dev_settings &&
+       python manage.py runserver 0.0.0.0:8000　--settings app.dev_settings"
      environment:
        - FRONT_URI=http://localhost:3000
        - ALLOWED_HOST=localhost
        - DB_HOST=db
        - DB_NAME=app
        - DB_USER=postgres
        - DB_PASS=supersecretpassword
      depends_on:
        - db
・・・
```

以下のコマンドにて、DBを更新して、管理者ユーザーを作成します。

```
$ make migrate
$ make admin
```

コンテナを再起動して、localhost:8000/adminにアクセスします。
コンテナが正しく動作していれば、管理者用のログインページが表示されるので、
先ほど作成した管理者にてログインします。
ログインすると、UserモデルとProfileモデルのCRUD操作ができるページに遷移します。

![Django管理者後のログインページ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/634526/68e005bb-96fc-4509-4e08-ee311584d81b.png)


# まとめ　　

今回はsettingsの修正と UserモデルとProfileモデルの作成をしました。
次回はDjangoをGraphqlとして、スキーマを作成して、ブラウザでデータを取得する所までを書きたいと思います。

次の記事: [Docker+Django+Next+TypeScript+ECSでアプリを作った話(3) ~ Djangoのスキーマ作成からデータ取得まで~](https://qiita.com/rys0707/items/94263ed21f489fb21aae)