https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/


Deployment checklist

The Internet is a hostile environment. Before deploying your Django project, you should take some time to review your settings, with security, performance, and operations in mind.

Django includes many security features. Some are built-in and always enabled. Others are optional because they aren’t always appropriate, or because they’re inconvenient for development. For example, forcing HTTPS may not be suitable for all websites, and it’s impractical for local development.

Performance optimizations are another category of trade-offs with convenience. For instance, caching is useful in production, less so for local development. Error reporting needs are also widely different.

The following checklist includes settings that:

  • must be set properly for Django to provide the expected level of security;
  • are expected to be different in each environment;
  • enable optional security features;
  • enable performance optimizations;
  • provide error reporting.

Many of these settings are sensitive and should be treated as confidential. If you’re releasing the source code for your project, a common practice is to publish suitable settings for development, and to use a private settings module for production.

Run manage.py check --deploy

Some of the checks described below can be automated using the check --deploy option. Be sure to run it against your production settings file as described in the option’s documentation.

Critical settings

SECRET_KEY

The secret key must be a large random value and it must be kept secret.

Make sure that the key used in production isn’t used anywhere else and avoid committing it to source control. This reduces the number of vectors from which an attacker may acquire the key.

Instead of hardcoding the secret key in your settings module, consider loading it from an environment variable:

import os
SECRET_KEY = os.environ['SECRET_KEY']

or from a file:

with open('/etc/secret_key.txt') as f:
    SECRET_KEY = f.read().strip()

DEBUG

You must never enable debug in production.

You’re certainly developing your project with DEBUG = True, since this enables handy features like full tracebacks in your browser.

For a production environment, though, this is a really bad idea, because it leaks lots of information about your project: excerpts of your source code, local variables, settings, libraries used, etc.

Environment-specific settings

ALLOWED_HOSTS

When DEBUG = False, Django doesn’t work at all without a suitable value for ALLOWED_HOSTS.

This setting is required to protect your site against some CSRF attacks. If you use a wildcard, you must perform your own validation of the Host HTTP header, or otherwise ensure that you aren’t vulnerable to this category of attacks.

You should also configure the Web server that sits in front of Django to validate the host. It should respond with a static error page or ignore requests for incorrect hosts instead of forwarding the request to Django. This way you’ll avoid spurious errors in your Django logs (or emails if you have error reporting configured that way). For example, on nginx you might setup a default server to return “444 No Response” on an unrecognized host:

server {
    listen 80 default_server;
    return 444;
}

CACHES

If you’re using a cache, connection parameters may be different in development and in production. Django defaults to per-process local-memory caching which may not be desirable.

Cache servers often have weak authentication. Make sure they only accept connections from your application servers.

DATABASES

Database connection parameters are probably different in development and in production.

Database passwords are very sensitive. You should protect them exactly like SECRET_KEY.

For maximum security, make sure database servers only accept connections from your application servers.

If you haven’t set up backups for your database, do it right now!

STATIC_ROOT and STATIC_URL

Static files are automatically served by the development server. In production, you must define a STATIC_ROOT directory where collectstatic will copy them.

See Managing static files (e.g. images, JavaScript, CSS) for more information.

MEDIA_ROOT and MEDIA_URL

Media files are uploaded by your users. They’re untrusted! Make sure your web server never attempts to interpret them. For instance, if a user uploads a .php file, the web server shouldn’t execute it.

Now is a good time to check your backup strategy for these files.

HTTPS

Any website which allows users to log in should enforce site-wide HTTPS to avoid transmitting access tokens in clear. In Django, access tokens include the login/password, the session cookie, and password reset tokens. (You can’t do much to protect password reset tokens if you’re sending them by email.)

Protecting sensitive areas such as the user account or the admin isn’t sufficient, because the same session cookie is used for HTTP and HTTPS. Your web server must redirect all HTTP traffic to HTTPS, and only transmit HTTPS requests to Django.

Once you’ve set up HTTPS, enable the following settings.

Performance optimizations

Setting DEBUG = False disables several features that are only useful in development. In addition, you can tune the following settings.

Sessions

Consider using cached sessions to improve performance.

If using database-backed sessions, regularly clear old sessions to avoid storing unnecessary data.

CONN_MAX_AGE

Enabling persistent database connections can result in a nice speed-up when connecting to the database accounts for a significant part of the request processing time.

This helps a lot on virtualized hosts with limited network performance.

TEMPLATES

Enabling the cached template loader often improves performance drastically, as it avoids compiling each template every time it needs to be rendered. See the template loaders docs for more information.

Error reporting

By the time you push your code to production, it’s hopefully robust, but you can’t rule out unexpected errors. Thankfully, Django can capture errors and notify you accordingly.

LOGGING

Review your logging configuration before putting your website in production, and check that it works as expected as soon as you have received some traffic.

See Logging for details on logging.

ADMINS and MANAGERS

ADMINS will be notified of 500 errors by email.

MANAGERS will be notified of 404 errors. IGNORABLE_404_URLS can help filter out spurious reports.

See Error reporting for details on error reporting by email.


https://qiita.com/sand/items/80a67da0a44b042f0bc3


Reactアプリから Django Rest API を叩いてみる

1.Django Rest API と Reactアプリ

以前、「DjangoのページをReactで作る - Webpack4」という記事を書きました。DjangoのページでReactを使うための、開発環境の構築を紹介したものですが、これはどちらかと言えば、Djangoの開発環境にReactの開発環境を「従わせた」ものでした。BabelやWebpackの設定はDjangoの環境に合わせる形で手動で行いました。

今回はDjangoReactの開発環境を完全に独立させます。特にReactではcreate-react-appを使いますので、簡単に開発環境を構築できます。

  • (1)サーバは、DjangoプロジェクトでRest APIを開発・単体テスト
  • (2)クライアントは、create-react-appで開発・単体テスト
  • (3)サーバ側でCORS設定を行い、クライアントからRest APIにアクセスする

(1)と(2)はそれぞれ独立して開発を行い、それぞれに動作確認します。
その後(3)のCORSの設定を行い、クライアントとサーバの連結を確認します。

今回はTodoアプリを作成していきます。

環境としては、todo-reactというディレクトリの下に、djangotodoというDjangoプロジェクトと、frontendというcreate-react-appのプロジェクトを作成します。

todo-react
│
├── djangotodo
│   ├── db.sqlite3
│   ├── djangotodo
│   ├── manage.py
│   └── todos
├── frontend
    ├── db.json
    ├── node_modules
    ├── package-lock.json
    ├── package.json
    ├── public
    └── src

2.サーバサイド - djangotodo

サーバサイドでは、だいたい以下のような作業を行います。

  • DjangoでTodoプロジェクトを作る
  • djangorestframeworkをインストールしRest APIを構築する
  • 単体テストを行う
  • 来たるべき総合テストに備えて、django-cors-headersをインストールしCORSの設定を行っておく

2-1.Djangoプロジェクト作成

venvで環境を作ってから、Djangoのプロジェクトを開始します。

python -m venv todo-react
source todo-react/bin/activate
cd todo-react
pip freeze
pip install django
django-admin startproject djangotodo
cd djangotodo/

私の環境は、DjangoはRemoteサーバに構築していますので、サーバのドメイン名を入力してアクセスを許可します。

djangotodo/settings.py
---
ALLOWED_HOSTS = ["www.mypress.jp"]
---

DBを初期化します。

python manage.py migrate

ここまででDjangoのアプリを立ち上げます。

python manage.py runserver 0:8080

http://www.mypress.jp:8080/ で、Djangoの初期画面を確認できます。

2-2.Todoアプリ作成

DjangoでTodoアプリを作成します

django-admin startapp todos

TodoアプリのModelを定義します。

todos/models.py
# todos/models.py
from django.db import models

class Todo(models.Model):
    title = models.CharField(max_length=200)
    description = models.TextField()
    status = models.CharField(default='Unstarted', max_length=100)

    def __str__(self):
        """A string representation of the model."""
        return self.title

TodoアプリをINSTALLED_APPSに追加します。

djangotodo/settings.py
---
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'todos', # New
]
---

migrationファイルを作成して、DBに反映させます。

python manage.py makemigrations todos
python manage.py migrate todos

adminを設定して、管理画面からTodoテーブルの操作を行えるようにします。

todos/admin.py
# todos/admin.py
from django.contrib import admin

from .models import Todo

admin.site.register(Todo)

管理者を追加します

python manage.py createsuperuser

サーバを起動します。

python manage.py runserver 0:8080

管理画面にアクセスします。

http://www.mypress.jp:8080/admin/

ログインします。
image.png

Todoを追加し、テストデータを作っておきます。
image.png

2-3.Django Rest Frameworkの設定

Djangoには、djangorestframeworkというRest APIを簡単に構築できるライブラリがあります。
Django Rest Framework with React Tutorial

インストールします。

pip install djangorestframework

INSTALLED_APPSを更新します。

djangotodo/settings.py
---
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rest_framework', # New
    'todos',
]

# New
REST_FRAMEWORK = {
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.AllowAny',
    ],
    'EXCEPTION_HANDLER': 'djangotodo.todos.utils.custom_exception_handler'
}
---

EXCEPTION_HANDLERはデバッグのために設定しました。これを使うためには、以下のコードも必要になります。

todos/utils.py
from rest_framework.views import exception_handler

def custom_exception_handler(exc, context):
    # Call REST framework's default exception handler first,
    # to get the standard error response.
    response = exception_handler(exc, context)

    # Now add the HTTP status code to the response.
    if response is not None:
        response.data['status_code'] = response.status_code

    return response

Django Rest Frameworkの設定のため、以下の3つを定義します。

  • urls.py :URLルート
  • serializers.py :dateをJSONに変換
  • views.py :APIエンドポイントにロジックを適用

この辺を詳しく知るためには、以下の公式ドキュメントを最初に読みましょう。
Tutorial 1: Serialization

URL Pathの定義です。リクエスト時のパスは末尾がスラッシュ(/)で終わっている必要があります。

djangotodo/urls.py
from django.urls import path, include  # New
from django.contrib import admin

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/', include('todos.urls')), # New
]

todoのURL Pathの定義です

todos/urls.py
# todos/urls.py
from django.urls import path

from . import views

urlpatterns = [
    path('', views.ListTodo.as_view()),
    path('<int:pk>/', views.DetailTodo.as_view()),
]

serializersとは、ざっくり言って、modelデータをJSONで出力するための機能です。ここではModelSerializer classを利用しているので、とてもシンプルに定義できます。SnippetSerializer classを利用する方法もありますが、この場合createやupdateの明示的な定義が必要になり複雑です。

todos/serializers.py
# todos/serializers.py
from rest_framework import serializers
from .models import Todo

class TodoSerializer(serializers.ModelSerializer):
    class Meta:
        fields = (
            'id',
            'title',
            'description',
            'status',
        )
        model = Todo

rest_frameworkを使って、viewsを定義します。

todos/views.py
# todos/views.py
from django.http import HttpResponse, JsonResponse
from django.views.decorators.csrf import csrf_exempt
from rest_framework.parsers import JSONParser
from todos.models import Todo
from todos.serializers import TodoSerializer


@csrf_exempt
def todo_list(request):
    """
    List all todos, or create a new todo.
    """
    if request.method == 'GET':
        todos = Todo.objects.all()
        serializer = TodoSerializer(todos, many=True)
        return JsonResponse(serializer.data, safe=False)

    elif request.method == 'POST':
        data = JSONParser().parse(request)
        serializer = TodoSerializer(data=data)
        if serializer.is_valid():
            serializer.save()
            return JsonResponse(serializer.data, status=201)
        return JsonResponse(serializer.errors, status=400)


@csrf_exempt
def todo_detail(request, pk):
    """
    Retrieve, update or delete a todo.
    """
    try:
        todo = Todo.objects.get(pk=pk)
    except Todo.DoesNotExist:
        return HttpResponse(status=404)

    if request.method == 'GET':
        serializer = TodoSerializer(todo)
        return JsonResponse(serializer.data)

    elif request.method == 'PUT':
        data = JSONParser().parse(request)
        serializer = TodoSerializer(todo, data=data)
        if serializer.is_valid():
            serializer.save()
            return JsonResponse(serializer.data)
        return JsonResponse(serializer.errors, status=400)

    elif request.method == 'DELETE':
        todo.delete()
        return HttpResponse(status=204)

今回はロジックを明示的に記述する仕方でviews.pyを定義しましたが、慣れたらgeneric class-based viewsを使った方が良いでしょう。コーディング量を劇的に減らすことが可能です。
Tutorial 3: Class-based Views

2-4.単体テスト

ブラウザからアクセスしてみます。

http://www.mypress.jp:8080/api/

先ほど管理画面から入力したテストデータが表示されます。
image.png

rest_frameworkはTodoの追加フォームも表示してくれます。便利です。

HTTPieコマンドを使っても簡単にテストできます。
HTTPie—aitch-tee-tee-pie—is a command line HTTP client with an intuitive UI

例えば以下のコマンドで「タスク追加」を確認できます。

http POST http://www.mypress.jp:8080/api/  title=a description=b status=Unstarted

2-5.CORS

これは本来なら、frontendのreactアプリ作成後の、最後に設定し確認するものです。しかしサーバでの設定ですので、ここでやっておきます。

また、CORSの確認テストで試行錯誤する時には、その都度必ずブラウザのキャッシュをクリアーすることを強くお勧めします。私はこれを怠り嵌りました!

Access to XMLHttpRequest at 'http://www.mypress.jp:8080/api' from 
origin 'http://www.mypress.jp:3000' has been blocked by 
CORS policy: No 'Access-Control-Allow-Origin' header is present on 
the requested resource.

以上のエラーを回避するためにサーバ側でCORSの設定を行う必要があります。django-cors-headersをインストールします。

pip install django-cors-headers

settings.pyを更新します。newコメントがついている4か所が修正箇所です。CorsMiddlewareはトップに置く必要があります。

djangotodo/settings.py
---
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rest_framework',
    'corsheaders', # new
    'todos',
]

REST_FRAMEWORK = {
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.AllowAny',
    ],
    'EXCEPTION_HANDLER': 'djangotodo.todos.utils.custom_exception_handler'
}

MIDDLEWARE = [
    'corsheaders.middleware.CorsMiddleware', # new topに置く
    'django.middleware.common.CommonMiddleware', # new
    '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',
]


# new
CORS_ORIGIN_WHITELIST = [
    'http://www.mypress.jp:3000',
]
# CORS_ORIGIN_ALLOW_ALL = False
---

以上でCORSの設定は終わりです。動作確認はReactアプリ完成後に行います。

3.フロントエンド - frontend

Reactプログラムは、reduxとredux-thunkを使い、action(非同期関数)から、Rest APIを叩きます。APIはaxiosで実装します。また最低限のUIを実装し、CSSを含めたコーディング量を減らすため、antdも利用します。一応最後に、全ソースを掲載します。少し長くなるのですが、不明な点を無くすため。

3-1.Reactプロジェクト作成

create-react-appを使って、Reactプロジェクトを作成します。必要なパッケージをインストールします。

create-react-app frontend
cd frontend
npm install --save axios react-redux redux redux-thunk redux-devtools-extension antd

package.jsonは以下の通りです。

package.json
{
  "name": "frontend",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "antd": "^3.20.5",
    "axios": "^0.19.0",
    "react": "^16.8.6",
    "react-dom": "^16.8.6",
    "react-redux": "^7.1.0",
    "react-scripts": "3.0.1",
    "redux": "^4.0.4",
    "redux-devtools-extension": "^2.13.8",
    "redux-thunk": "^2.3.0"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },
  "eslintConfig": {
    "extends": "react-app"
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  }
}

3-2.redux-devtools-extension

redux-devtools-extensionはReduxアプリの開発ツールの一つで、ブラウザの拡張機能からReduxの状態管理を可視化してくれます。別途、Chromeの拡張機能を設定する必要があります。

以下の画面のように、Chromeの拡張機能で専用ウィンドが開き、Reduxのactionやstateが可視化されます。
image.png

3-3.antd

React のUI libraryであるantdを使います。特にListコンポネントを使うことで、ソースコードをとても簡潔にすることができました。入力フォームにはFormコンポーネントを使いました。

React UI library の antd について (1) - Button

個人的には、antdを使うことにより、面倒なstyleを指定することが少なくなるので助かります。

3-4.単体テスト

Djangoと結合する前に、json-serverを使って単体テストを行います。

frontendディレクトリ直下にdb.jsonファイルを作ります。

db.json
{
  "api": [
    {
      "id": 1,
      "title": "Reduxのお勉強",
      "description": "特に非同期actionについて",
      "status": "In Progress"
    },
    {
      "id": 2,
      "title": "ES6のお勉強",
      "description": "Promiseについて",
      "status": "In Progress"
    },
    {
      "id": 3,
      "title": "朝食",
      "description": "忘れずに食べること",
      "status": "Completed"
    },
    {
      "title": "掃除",
      "description": "要らない本は捨てる",
      "status": "In Progress",
      "id": 4
    },
    {
      "title": "草刈り",
      "description": "夏草に要注意!",
      "status": "Unstarted",
      "id": 5
    }
  ]
}

frontendディレクトリ直下で、json-serverを起動します。

json-server --host www.mypress.jp --watch db.json -p 3003

状態「Unstarted」、「In Progress」、「Completed」毎にTodo一覧が表示されます。以下の画面になります。

image.png

3-5.ソースコード

frontend/src直下のソースツリーです。

├── App.js
├── actions
│   └── index.js
├── api
│   └── index.js
├── components
│   ├── FlashMessage.js
│   ├── TaskList.js
│   └── TasksPage.js
├── constants
│   └── index.js
├── index.js
└── reducers
    └── index.js

(1)トップ

主に、Reduxの設定を行い、App.jsを呼びます。

src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import { Provider } from 'react-redux';
import { composeWithDevTools } from 'redux-devtools-extension';
import tasksReducer from './reducers';
import App from './App';

const rootReducer = (state = {}, action) => {
  return {
    tasks: tasksReducer(state.tasks, action),
  };
};

const store = createStore(
  rootReducer,
  composeWithDevTools(applyMiddleware(thunk))
);

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);

App.jsはメイン画面の枠組みの定義です。

src/App.js
import React, { Component } from 'react';
import { connect } from 'react-redux';
import TasksPage from './components/TasksPage';
import FlashMessage from './components/FlashMessage';
import { createTask, editTask, deleteTask, fetchTasks } from './actions';
import 'antd/dist/antd.css';

class App extends Component {
  componentDidMount() {
    this.props.dispatch(fetchTasks());
  }

  onCreateTask = ({ title, description }) => {
    this.props.dispatch(createTask({ title, description }));
  };

  onStatusChange = (id, status) => {
    this.props.dispatch(editTask(id, { status }));
  };

  onDeleteTask = (id) => {
    this.props.dispatch(deleteTask(id));
  };

  render() {
    return (
      <div>
        {this.props.error && <FlashMessage message={this.props.error} />}
        <div>
          <TasksPage
            tasks={this.props.tasks}
            onCreateTask={this.onCreateTask}
            onStatusChange={this.onStatusChange}
            isLoading={this.props.isLoading}
          />
        </div>
      </div>
    );
  }
}

function mapStateToProps(state) {
  const { tasks, isLoading, error } = state.tasks;
  return { tasks, isLoading, error };
}

export default connect(mapStateToProps)(App);

(2)Reducer & Action

reducerの定義です

src/reducers/index.js
const initialState = {
  tasks: [],
  isLoading: false,
  error: null,
};

export default function tasks(state = initialState, action) {
  switch (action.type) {
    case 'FETCH_TASKS_STARTED': {
      return {
        ...state,
        isLoading: true,
      };
    }
    case 'FETCH_TASKS_SUCCEEDED': {
      return {
        ...state,
        tasks: action.payload.tasks,
        isLoading: false,
      };
    }
    case 'FETCH_TASKS_FAILED': {
      return {
        ...state,
        isLoading: false,
        error: action.payload.error,
      };
    }
    case 'CREATE_TASK_SUCCEEDED': {
      return {
        ...state,
        tasks: state.tasks.concat(action.payload.task),
      };
    }
    case 'EDIT_TASK_SUCCEEDED': {
      const { payload } = action;
      const nextTasks = state.tasks.map(task => {
        if (task.id === payload.task.id) {
          return payload.task;
        }

        return task;
      });
      return {
        ...state,
        tasks: nextTasks,
      };
    }
    case 'DELETE_TASK_SUCCEEDED': {
      const { payload } = action;
      const nextTasks = state.tasks.filter(task => task.id !== payload.id)
      return {
        ...state,
        tasks: nextTasks,
      };
    }
    default: {
      return state;
    }
  }
}

actionの定義です

src/actions/index.js
import * as api from '../api';

function fetchTasksSucceeded(tasks) {
  return {
    type: 'FETCH_TASKS_SUCCEEDED',
    payload: {
      tasks,
    },
  };
}

function fetchTasksFailed(error) {
  return {
    type: 'FETCH_TASKS_FAILED',
    payload: {
      error,
    },
  };
}

function fetchTasksStarted() {
  return {
    type: 'FETCH_TASKS_STARTED',
  };
}

export function fetchTasks() {
  return dispatch => {
    dispatch(fetchTasksStarted());

    api
      .fetchTasks()
      .then(resp => {
        dispatch(fetchTasksSucceeded(resp.data));
      })
      .catch(err => {
        dispatch(fetchTasksFailed(err.message));
      });
  };
}

function createTaskSucceeded(task) {
  return {
    type: 'CREATE_TASK_SUCCEEDED',
    payload: {
      task,
    },
  };
}

export function createTask({ title, description, status = 'Unstarted' }) {
  return dispatch => {
    api.createTask({ title, description, status }).then(resp => {
      dispatch(createTaskSucceeded(resp.data));
    });
  };
}

function editTaskSucceeded(task) {
  return {
    type: 'EDIT_TASK_SUCCEEDED',
    payload: {
      task,
    },
  };
}

export function editTask(id, params = {}) {
  return (dispatch, getState) => {
    const task = getTaskById(getState().tasks.tasks, id);
    const updatedTask = Object.assign({}, task, params);
    api.editTask(id, updatedTask).then(resp => {
      dispatch(editTaskSucceeded(resp.data));
    });
  };
}


function getTaskById(tasks, id) {
  return tasks.find(task => task.id === id);
}


function deleteTaskSucceeded(id) {
  return {
    type: 'DELETE_TASK_SUCCEEDED',
    payload: {
      id,
    },
  };
}

export function deleteTask(id) {
  return (dispatch, getState) => {
    api.deleteTask(id).then(resp => {
      console.log(resp)
      dispatch(deleteTaskSucceeded(id));
    });
  };
}

statusの定数の定義です。

src/constants/index.js
export const TASK_STATUSES = ['Unstarted', 'In Progress', 'Completed'];

(3)API

Rest APIのインターフェースモジュールです。ここで注意が必要なのは、DjangoのPOSTの場合、パスの末尾に、'/api/' のように、スラッシュが必要だということです。 '/api' ではだめです。

'/api' のGETの場合、自動的に末尾にスラッシュを付け直してリダイレクトしてOKになります。POSTでもリダイレクトしてくれますが、リダイレクト時にPOST dataが落ちてしまい、結果的にエラーとなります。
json-serverではテストが通ってもDjangoではNGになるので注意が必要です。

POSTでBAD Requestエラーとなる場合は、paramsの中身もチェックしてみましょう。私はここで躓いて、actionが正しいデータを渡してくれているのかを確認せずに、半日も悩んでしまいました。

src/api/index.js
import axios from 'axios';

// const API_BASE_URL = 'http://www.mypress.jp:3003'; // json-server用
const API_BASE_URL = 'http://www.mypress.jp:8080'; // Django用

const client = axios.create({
  baseURL: API_BASE_URL,
  headers: {
    'Content-Type': 'application/json'
  },
});

export function fetchTasks() {
  return client.get('/api/');
}

export function createTask(params) {
  console.log(params)
  return client.post('/api/', params);
}

export function editTask(id, params) {
  return client.put(`/api/${id}`, params);
}

export function deleteTask(id) {
  return client.delete(`/api/${id}/`);
}

(4)components

TasksPage.jsはTasksPageクラスの他に。タスク追加フォームであるAddTaskFormクラスを定義しています。別ファイルにすべきかと思いましたが、面倒なので一緒にしました。

タスク追加フォームには、antdForm componentを使っています。validateが統一的に行えるので便利ですが、少しコードが複雑になります。詳しくは「React UI library の antd について (3) - redux-form」も参照してください。

src/components/TasksPage.js
import React, { Component } from 'react';
import { Form, Input, Icon, Button } from 'antd';

import TaskList from './TaskList';
import { TASK_STATUSES } from '../constants';

class TasksPage extends Component {
  constructor(props) {
    super(props);
    this.state = {
      showNewCardForm: false,
    };
  }

  toggleForm = () => {
    this.setState({ showNewCardForm: !this.state.showNewCardForm });
  };

  render() {
    if (this.props.isLoading) {
      return (
        <div>
          Loading...
        </div>
      );
    }

    return (
      <div>
        <div>
          <Button type="primary" onClick={this.toggleForm}>+タスク追加</Button>
        </div>
        {this.state.showNewCardForm && <WrappedAddTaskForm onCreateTask={this.props.onCreateTask} />}
        <div>
          {TASK_STATUSES.map(status => {
            const statusTasks = this.props.tasks.filter(
              task => task.status === status
            );
            return (
            <div style={{ margin: "25px 20px 25px 20px" }}>
              <h2>{status}</h2>
              <TaskList
                key={status}
                status={status}
                tasks={statusTasks}
                onStatusChange={this.props.onStatusChange}
                onDeleteTask={this.props.onDeleteTask}
              />
            </div>
            );
          })}
        </div>
      </div>
    );
  }
}

export default TasksPage;



class AddTaskForm extends React.Component {
  componentDidMount() {
    // To disabled submit button at the beginning.
    this.props.form.validateFields();
  }

  handleSubmit = e => {
    e.preventDefault();
    this.props.form.validateFields((err, values) => {
      if (!err) {
        console.log('Received values of form: ', values);
        this.props.onCreateTask(values)
      }
    });
  };

  render() {
    const { getFieldDecorator, getFieldError, isFieldTouched } = this.props.form;

    // Only show error after a field is touched.
    const taskError = isFieldTouched('task') && getFieldError('task');
    const descriptionError = isFieldTouched('description') && getFieldError('description');
    const buttonDisable = getFieldError('task') || getFieldError('description')

    return (
      <Form layout="inline" onSubmit={this.handleSubmit}>
        <Form.Item validateStatus={taskError ? 'error' : ''} help={taskError || ''}>
          {getFieldDecorator('task', {
            rules: [{ required: true, message: 'taskを入力してください!' }],
          })(
            <Input
              prefix={<Icon type="user" style={{ color: 'rgba(0,0,0,.25)' }} />}
              placeholder="task"
            />,
          )}
        </Form.Item>
        <Form.Item validateStatus={descriptionError ? 'error' : ''} help={descriptionError || ''}>
          {getFieldDecorator('description', {
            rules: [{ required: true, message: 'descriptionを入力してください!' }],
          })(
            <Input
              prefix={<Icon type="lock" style={{ color: 'rgba(0,0,0,.25)' }} />}
              placeholder="description"
            />,
          )}
        </Form.Item>
        <Form.Item>
          <Button type="primary" htmlType="submit" disabled={buttonDisable}>
            タスク追加
          </Button>
        </Form.Item>
      </Form>
    );
  }
}

const WrappedAddTaskForm = Form.create({ name: 'add_task_form' })(AddTaskForm);

タスク一覧の表示です。antdのList componentを使っているので、とても簡潔に記述できています。

src/components/TaskList.js
import React from 'react';
import { List, Card } from 'antd';
import { TASK_STATUSES } from '../constants';


const TaskList = props => {
  return (
    <List
      grid={{ gutter: 16, column: 4 }}
      dataSource={props.tasks}
      renderItem={item => (
        <List.Item>
          <Card title={item.title}>{item.description}</Card>
          <select value={item.status} onChange={(e) => {onStatusChange(e, item.id)}}>
            {TASK_STATUSES.map(status => (
              <option key={status} value={status}>{status}</option>
            ))}
          </select>
          <Button type="danger" onClick={()=>{props.onDeleteTask(item.id)}}>
            タスク削除
          </Button>
        </List.Item>
      )}
    />
  );

  function onStatusChange(e, id) {
    props.onStatusChange(id, e.target.value);
  }
};

export default TaskList;

actionでエラーが発生した場合に、表示されるメッセージです。

src/components/FlashMessage.js
import React from 'react';

export default function FlashMessage(props) {
  return (
    <div>
      {props.message}
    </div>
  );
}

FlashMessage.defaultProps = {
  message: 'An error occurred',
};

4.ReactとDjangoの結合

現在は以下の状況です

  • 【サーバサイド】Django単体での動作を確認済み
  • 【フロントエンド】React単体での動作を確認済み

最後にサーバサイドとフロントエンドを結合して動作を確認します。
単体で成功しても、結合で失敗し時間を費やすことになるのは、よくあることです。今回も以下の2点でだいぶ時間を浪費してしまいました。

  • CORSの設定のデバッグに時間を要した(ブラウザキャッシュの問題)
  • POSTリクエストエラーに時間を要した(リクエストパスの末尾のスラッシュを忘れた & POSTデータの属性"title"が間違っていた)

特にjson-serverはサーバ側は、特にチェック無しで通りますが、Djangoの場合はデータの属性名が違っていたりすると、当然はじかれます。この点を忘れて迷路に迷うことにならないように注意します。

単体の時と同じですが、結合時の画面です。
image.png

今回は以上です。


https://qiita.com/karintou/items/52ee1f7c5fa641980188

はじめに

「とりあえずなんとかする方法は知っているけど、なんでそれでいいのかわからない」という疑問を解消していきます。
今回はCORS編です。
長くなるので、時間がない方は先に結論を見てください。

問題となるエラーについて

APIサーバーとWebサーバーをそれぞれ別ポートで立てている時、以下のようなエラーが出る場合があります。

CORS-Error

これは、開発環境でAngular(localhost:4200)からDjango(localhost:8000)へAPIリクエストを投げた時に起きるエラーです。
「APIサーバー(localhost:8000)へのCross-Originなリダイレクトは、Cross-Origin Resource Sharing policyによって拒否されました」と怒られます。
このエラーの意味するところから理解し、対策・解決方法を探っていきます。

Cross-Origin

オリジン

オリジンとは、「プロトコル」+「ホスト」+「ポート番号」の組み合わせのことです。
上記の例でいうと

サーバー プロトコル ホスト ポート番号
Angular http localhost 4200
Django http localhost 8000

ポート番号が異なるので、別のオリジンとなります。

Cross-Origin

では、なぜ別のオリジン間(Cross-Origin)の通信が問題になるのでしょうか。
CSRF(Cross Site Request forgeries)が考えられるからです。

CSRFは以下の手順で実行されます。

  1. 不正なアクセスを行うスクリプトを仕込んだページを用意する
  2. ユーザーにアクセスさせ、攻撃用ページを踏ませる
  3. アクセスしたユーザーから不正なアクセスが行われる

不正なアクセス自体はユーザーから送信されるので攻撃者は発覚しない、という手法です。

この攻撃方法は、あるドメインから読み込まれたページから、別のドメインへのアクセスが許可されている脆弱性をついています。
そのため、通常はオリジン間のHTTPリクエストは制限されます。
今回のエラーも、localhost:4200から読み込まれたページで実行される、localhost:8000へのHTTPリクエストが制限に引っかかったため生じました。

DjangoのCSRF対策

次に、DjangoのCSRF対策について確認します。
公式によると

  • settings.pyのMIDDLEWAREに 'django.middleware.csrf.CsrfViewMiddleware'が入っていると有効になる(デフォルトで有効)
  • POST, PUT, DELETEのような変更を伴うリクエストに対してチェックを行う
  • レスポンス時、クッキーに「csrftoken」というキーでトークンを発行する
  • リクエストに「X-CSRFToken」というヘッダーに発行したトークンが入っているかをチェック
    • 入っていたらアクセス許可
    • 入っていない、またはトークンが違う場合、403(Forbidden)エラーを返す

となっています。

CORS

次に、対策・解決方法を考えていきます。
オリジン間の制限を変更することで、別オリジン間のHTTPリクエストを許可すれば良いはずです。
この仕組みをCORS(Cross-Origin Resource Sharing)と言います。
設定可能なヘッダーについては公式ぺージを参考にしてください。

対策

基本的にデータを取得される側(APIサーバー)でアクセス許可設定を行います。
エラーの文面をもう一度見ると、
「localhost:4200はAccess-Control-Allow-Originで許可されていません」と怒られています。
なので、レスポンスに「Access-Control-Allow-Origin」というヘッダーを追加します。
後ほど別の方法は紹介しますが、テストのためにDjangoのViewを以下のように変更します。

# なんらかのレスポンスを返すView関数
def hoge(request):
    # ~~~なんらかのデータを取得してJsonResponseで返す~~~~
    response = JsonResponse(data)
    response['Access-Control-Allow-Origin'] = 'localhost:4200'
    return response

これでレスポンスが正常に返ってきます。
レスポンスヘッダーは以下のようになっています。

Access-Control-Allow-Origin Header

ただし、POST、DELETEなどのメソッドだとうまくいかないケースがあります。
エラーとしては以下のようになります。

Preflight Error

 

Preflight Header Error

見知らぬ単語「Preflight」が出てきました。次はこのエラーに取り掛かりましょう。

Preflight

特定のメソッドや、特定のヘッダーがリクエストに入っていると、実際のリクエストを投げる前に、別ドメインの送信相手に安全確認をとる仕様となっています。
この事前リクエストをPreflightリクエストと言います。
別ドメインからのPreflightリクエストに対し、APIサーバーがどのようなリクエストならアクセスを許可するかを返し、そのレスポンスが返ってきてから実際のリクエストを投げます。

上記のエラーは、

  1. Preflightリクエストに対するレスポンスが返ってきていない
  2. 実際のリクエストのContent-Typeヘッダーが許可されていない
    ことからエラーになっているようです。

先ほど確認したDjangoのCSRF対策と合わせて考えると、

  1. Preflightメソッドに対してレスポンスを返す
  2. クッキーの「csrftoken」をヘッダーに追加する

ことで解決するはずです。

これもまた別の方法がありますが、テストのためにViewを以下のように変更します。

# View関数
def hoge(request):
    # Preflightに対するレスポンスを返す(1)
    if request.method == 'OPTIONS':
        response = HttpResponse()
        response['Access-Control-Allow-Origin'] = 'http://localhost:4200'
        response['Access-Control-Allow-Credentials'] = 'true'
        response['Access-Control-Allow-Headers'] = "Content-Type, Accept, X-CSRFToken"
        response['Access-Control-Allow-Methods'] = "POST, OPTIONS"
        return response
    else:
        # ~~~なんらかの処理をしてJsonResponseで返す~~~~
        response = JsonResponse(data)
        response['Access-Control-Allow-Origin'] = 'localhost:4200' 
        response['Access-Control-Allow-Credentials'] = 'true'
        return response

また、Angular側で、ヘッダーにトークンを追加します。

import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent, HttpXsrfTokenExtractor } from '@angular/common/http';
import { Observable } from 'rxjs/Observable';

@Injectable()
export class HttpXsrfInterceptor implements HttpInterceptor {

  constructor(private tokenExtractor: HttpXsrfTokenExtractor) {
  }
  // httpリクエストに対してヘッダーを追加する(2)
  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    const headerName = 'X-CSRFToken';
    const token = this.tokenExtractor.getToken() as string;
    if (token !== null && !req.headers.has(headerName)) {
      req = req.clone({ headers: req.headers.set(headerName, token) });
    }
    return next.handle(req);
  }
}

また、app.module.tsも変更します。

@NgModule({

  imports: [
    // 追加。トークンが入っているクッキー名とヘッダー名を指定(2)
    HttpClientXsrfModule.withOptions({cookieName: 'csrftoken', headerName: 'X-CSRFToken'})
  ],
  providers: [
    // サービスの登録
    { provide: HTTP_INTERCEPTORS,useClass: HttpXsrfInterceptor,  multi: true },
  ],
}

また、HttpClientのpostメソッドのoptionsに{withCredentials: true}を追加します。
これでPOSTリクエストも成功します。

django-cors-headers

以上で別オリジンからのリクエストを正常に処理できるようになりました。
ただ、Djangoには便利なライブラリがあります。django-cors-headersです。
今までは確認のためにviewで返すリクエストに直接ヘッダーを追加していましたが、django-cors-headersを使用し、設定をすれば勝手にヘッダーを追加してくれます。

利用法

インストール
pip install django-cors-headers

設定追加

# 追加分のみ
INSTALLED_APPS = [
    'corsheaders'
]

# 上から順に実行されるので、CommonMiddleWareより上に挿入
MIDDLEWARE = [
    'corsheaders.middleware.CorsMiddleware',
 ]

# 許可するオリジン
CORS_ORIGIN_WHITELIST = [
    'localhost:4200',
]
# レスポンスを公開する
CORS_ALLOW_CREDENTIALS = True

これだけです。
そのほかにも設定があるので、状況によって追加・変更を行います。

結論

  • django-cors-headersを使う
    • またはレスポンスにヘッダーを追加する
  • Angularで
    • クッキーに入っている「csrftoken」を「X-CSRFToken」ヘッダーに追加
    • HttpClientのメソッドオプションの「withCredentials」をtrueにする
    • HttpClientXsrfModule.withOptionsを設定する

以上で解決するはずです。
自分はAngular側の設定で時間を溶かしてしまいました...。
何かあればコメント・指摘等お願いします。

参考ページ

https://developer.mozilla.org/ja/docs/Web/HTTP/HTTP_access_control
https://www.trendmicro.com/ja_jp/security-intelligence/research-reports/threat-solution/csrf.html
https://stackoverflow.com/questions/46040922/angular4-httpclient-csrf-does-not-send-x-xsrf-token
https://github.com/ottoyiu/django-cors-headers

django-rest-framework(DRF)뭐야?

DRF는 장고프레임워크를 사용해 api를 간단하게 만들 수 있는 프레임워크다. aws를 해본사람은 알겠지만, api gateway같은 걸 장고프레임워크를 통해 자작, 커스터마이징한다고 생각하면 된다.

그런데, 간단히 사용할 수 있는 api프레임워크도 많다. 가령 flask 같은 거. 실제 현재 로컬 개발 중 flask를 통해 api를 반환시키고 있다. DRF는 왜 사용되는 걸까?

각종 사이트에서 소개하는 django-rest-framework(DRF)를 사용하는 이유

django-rest-framework공식 홈페이지에서 설명하는 DRF를 사용하는 이유

Django REST framework is a powerful and flexible toolkit for building Web APIs.

Some reasons you might want to use REST framework:

data-flair에서 설명하는 DRF를 사용하는 이유https://data-flair.training/blogs/django-rest-framework/

DRF makes it easier for a developer to debug their APIs. The other big feature of DRF is that it converts the Models into serializers. What we have done in this Django REST Framework tutorial is simply made classes and extended them with built-in classes. If we needed to have this kind of architecture, we would need so much more code in place. DRF made it so easy for us to serialize data.
 
The serializers not only take our models and transmit them as JSON objects. They also provide data from users to the backend. They automatically clean the data or validate data. This is a plus from DRF. Cleaned data removes all the security issues.
 
Believe me, the security provided by DRF is well established. Django REST Framework has many more features than explained here. They go well beyond the scope of just Django. You will need a better understanding of APIs and REST architecture. It is not so difficult but will take time.

내가 개인적으로 생각하는 DRF를 사용하는 이유

  1. serialize가 편하게 된다. 이거 편하다.
  2. api gateway는 managed서비스 이기 때문에 여러가지 중요 기능들이 사용자도 모르는 사이에 이미 잘 구현되어 있습니다. 가령, validation이나 security같은 것들이죠. 이걸 일일이 개발자가 구현해 api를 만든다고 하면 꽤나 복잡해집니다. validation이나 security같은 고급 기능 없이 flask는 단순히 http request를 패싱해주는 기능만 합니다. 여기에 validation이나 security기능들을 붙인다고 생각하면 개발자의 부담은 상당히 늘어나게 됩니다.
  3. rest api를 개발 하는데 있어서, rest api 규약을 지킬수 있도록 편리 기능들을 제공해줍니다.

https://blueshw.github.io/2016/03/03/django-using-custom-templatetags/



커스텀 템플릿태그(templatetags) 활용하기
March 03, 2016
웹 개발을 하다보면, html 코드 상에서 다양한 연산을 해야하는 경우가 발생합니다. 그래서 php, jsp, asp, jade 등 각 언어별 웹 프레임워크에서 이와 같은 경우를 처리해주기 위한 기능을 제공하고 있습니다. 장고(django) 템플릿(template)에서도 위와 같은 웹 프레임워크와 같이 동일한 기능을 지원하는 템플릿태그(templatetags)라는 것이 있습니다. 장고의 템플릿태그는 다른 웹 프레임워크와 마찬가지로 기본적으로 개발자가 필요한 기능은 대부분 제공하고 있습니다.

웹 프레임워크가 기본적인 기능을 대부분 제공하고 있지만, 개발을 하다보면 자신이 원하는 기능이 없는 경우가 간혹 있습니다. 그래서 장고에서는 개발자가 커스텀으로 템플릿태그를 만들수 있는 기능을 제공하고 있습니다.

우선 아래와 같이 앱(app) 아래에 templatetags 라는 폴더를 만들어 줍니다. temaplatetags 라는 폴더 이름은 고정값이므로 반드시 동일하게 생성합니다.

proj/
	app/
		__init__.py
		models.py
		view.py
		templatetags/
			__init__.py
			custom_tags.py    (커스텀 템플릿태그를 저장할 모듈 파일)

이때, app 은 반드시 setting 파일의 INSTALLED_APPS 에 추가가 되어 있어야 합니다. 그리고 한가지 주의할 점은 여러 앱에 각각 templatetags 가 있는 경우, 모듈의 이름이 겹치지 않도록해야 합니다. 이유는, template 에서 커스텀 태그는 앱의 위치와 상관없이 모듈 이름으로 로드되므로 이름이 겹치게 되면 충돌이 발생하게 됩니다. 즉,

proj/
	app/
		templatetags/
			custom_tags.py
	common/
		templatetags/
			common_tags.py    (태그모듈 이름이 겹치지 않도록 주의!!!)

태그 모듈을 사용하는 방법은 간단합니다. 커스텀 태그를 사용하고자하는 템플릿 파일의 상단에 아래와 같이 한줄만 추가해주면 됩니다.

{% load custom_tags %}

그렇다면 실제로 커스텀 태그를 만들어서 사용하는 예제를 만들어 보도록 하겠습니다. custom_tags.py 파일을 열어 사용하려는 태그 이름으로 메서드 이름으로 지정하여 만들어줍니다.

@register.filter            # 1
def add_str(left, right):
return left + right

@register.simple_tag            # 2
def today():
return datetime.now().strftime("%Y-%m-%d %H:%M")

@register.assignment_tag            # 3
def max_int(a, b):
return max(int(a), int(b))

@register.inclusion_tag(div.html, takes_context=True)            # 4
def include_div(context):
return {
'div_param': context['param']
}

대략 위의 4 가지 태그로 구분할 수 있는데, 각 태그에 따라서 사용법이 조금씩 다릅니다. 이 4 가지 커스텀 태그만 이용하면 웬만한 기능은 다 만들어 낼 수 있습니다. 번호별 사용법은 아래와 같습니다.

# 1.
# filter 태그는 앞의 값(left)에다가 뒤의 값(right)을 연산하는 태그입니다.
# filter이기 때문에 여러개의 필터를 붙여서 사용가능합니다.
# add_str 메서드의 left 파라미터가 prefix에 해당하고, right 파라미터가 url에 해당합니다.
# 결과적으로 prefix + url이 add_str 메서드를 통해 div의 text가 되는 것이지요.

<div>
{{ prefix | add_str: url | add_str: name | add_str: params }}
</div>

# 2.
# simple_tag는 단순히 어떤 특정값을 출력합니다.
# 아래와 같이 today를 입력하면, "2016-3-2 10:00"과 같이 현재 시간이 출력됩니다.

<div>
{{ today }}
</div>

# 3.
# assignment_tag는 템플릿에서 사용가능한 변수에 결과를 저장하는 역할을 합니다.
# 어찌보면 with 태그와 유사한 형태라 할 수 있으나, with과는 다르게 {% endwith %} 처럼 끝을 맺어줄 필요가 없습니다.
# 즉, 좀 더 간편하게 변수를 설정해 줄 수 있고, 필요한 기능을 태그 모듈에 별도로 삽입할 수 있다는 장점이 있습니다.

{% max_int first_count second_count as max_count %}

# 4.
# inclusion_tag는 저도 프로젝트에 직접 사용해 보진 않았지만, 테스트는 해보았습니다.
# 간략히 설명해서 inclusion_tag를 사용하면 데코레이터의 첫번째 파라미터인 템플릿을 호출하여 부모 템플릿에 출력합니다.
# 이때, 호출되는 템플릿에 부모 템플릿(호출하는 템플릿)의 각종 파라미터를 전달해 줄 수 있습니다.
# 데코레이터의 takes_context=True로 설정해주면,
# 부모 템플릿의 context의 값을 가져와 호출하는 템플릿으로 전달할 수 있습니다.

parent.html
{{ include_div }}

div.html
<div>{{ div_param }}</div>

이상 커스텀 태그의 종류와 사용법에 대해서 알아보았습니다. 제가 설명드린 커스텀 태그는 아주 기초적인 부분이라 제작 및 사용법이 아주 간단한데요. 커스텀 태그 파일은 파이썬 모듈이기 때문에 파이썬에서 사용할 수 있는 내장함수와 모든 확장 모듈을 사용할 수 있기 때문에, 얼마든지 복잡하고 파워풀한 기능을 가진 태그를 만들어 낼 수 있습니다.

하지만, 복잡한 연산을 처리하는 것은 템플릿보다는 웹서버 단에서 처리하는 것이 우선이고, 서버에서 처리가 곤란하거나 불가피한 상황인 경우에 태그를 사용해서 처리하는 것이라 생각합니다. 아마 장고에서도 사용가능한 기본 태그를 최소한으로 만들어 놓은 것도 같은 이유 때문일 거라 생각이 드네요.




[Django] プロジェクト構成のベストプラクティスを探る - 2.設定ファイルを本番用と開発用に分割する

この記事は最終更新日から1年以上が経過しています。

この記事について

Djangoで本番運用やチーム開発を行うに当たって、プロジェクトの初期構成にどのように手を入れたらよいかを、The Twelve Factorsなどを参考にまとめたメモです。

参考:

設定ファイルの分割の目的

問題点

Djangoの設定ファイルを本番用と開発用に分ける方法としてはlocal_settings.pyを使う方法がよく紹介されます。

参考:https://djangogirlsjapan.gitbooks.io/workshop_tutorialjp/deploy/

しかし、Two Scoops of Django 1.11 ではこの方法に以下の問題点があると指摘しています。

  • 設定が増えてくると本番環境と開発環境の設定の違いがわかりづらい。
  • 開発環境の設定(local_settings.py)がリポジトリに無いので、開発ノウハウが開発者個人に留まり共有されない。

Djangoのベストプラクティスを集めたテンプレート出力ツール「cookiecutter-django」では、本番用と開発用の両方の設定ファイルを作成し、起動スクリプトでどちらを呼ぶか指定する方法を採っています。今回はこれを手法を模倣します。

設定方法

初期構成から変更を加えるファイルは以下の通りです。
設定ファイルを読み込む場所はプロジェクト内で2か所なので、そこを本番用と開発用に分けてしまうという方針です。


ここに加えて、local.pyはGit資源上存在せずgitignoreにしておく。production.pyやstaging.pyなどの設定ファイルはS3からデータを取得し、そのデータに基づきAnsibleのような自動化ツールによってファイルを追加する。

base_dir
  ├ manage.py               # [更新] 開発環境の起動スクリプト
  └ config
      ├ wsgi.py             # [更新] 本番環境の起動スクリプト
      └ settings            # [新規] 設定ファイルディレクトリ
          ├ __init__.py     # [新規] 
          ├ base.py         # [新規] 共通設定 ※元のsettings.pyを使う
          ├ local.py        # [新規] 開発環境設定
          └ production.py   # [新規] 本番環境設定

作業手順

手順1

まず元々の設定ファイルsettings.pyのあるディレクトリにsettingsディレクトリを作ります。

手順2

ディレクトリ「settings」にsettings.pyを移動して、ファイル名をbase.pyに変更します。

手順3

base.pyの最初の方のBASE_DIRの指定を編集します。設定ファイルの場所が1階層深くなるので入れ子を一つ増やします。

project/settings/base.py
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# ↓↓
BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

※ django-environを使って以下のようにもできます。

import environ
ROOT_DIR = environ.Path(__file__) - 3

手順4

個別設定になる項目を共通設定より削除します。

個別設定の例:

  • DEBUG:デバッグフラグ(開発:Ture / 本番:False)
  • DATABASE:データベース接続
  • MEDIA_ROOT等:ストレージ設定
  • STATIC_ROOT:静的ファイル配置
  • LOGGING:ログ出力

手順5

新規に開発用設定ファイル(local.py)と本番用設定(production.py)ファイルを作ります。ファイルの先頭には共通設定ファイルへの参照を記述します。

from .base import *

その後、それぞれの個別設定を追加します。

※ 共有設定の項目(配列)に追加する場合は以下の様にする

config/settings/local.py
INSTALLED_APPS += (
    'debug_toolbar', # and other apps for local development
)

手順6

起動スクリプトとなる manage.py(開発用)とwsgi.py(本番用)の設定ファイルの指定箇所をそれぞれ変更します。

変更前

manage.py,wsgi.py共通
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "project.settings")

変更後

manage.py
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.local")
config/wsgi.py
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.production")

注意点

本番環境でmanage.pyコマンドを実行する場合は、あらかじめ環境変数DJANGO_SETTINGS_MODULEconfig.settings.productionを設定するか、manage.pyの起動オプションに--settings config.settings.productionを指定する必要がある。

例:Heroku

heroku config:set DJANGO_SETTINGS_MODULE=mysite.settings.production


들어가면서

Django의 세션을 활용해, 유저별 데이터를 저장해볼까 합니다.
 
현재프로젝트에서는 Session을 활용하지 않고 redis로 Session을 커스터마이징 해서 사용했었는데요. redis로 session을 커스터마이징하면 sessionId도 직접 생성해야하고 (uuid로 생성해서 사용했습니다), django가 제공하는 session과의 유기적인 기능들을 사용하지 못하는 단점이 있었습니다.
 
그 결과 django의 세션기능을 활용하여 유저별 stateful한 웹어플을 만들어보고자 합니다. 심지어 사용법도 매우 간단합니다.

session이란 뭘까요?session

클라이언트별 정보를 브라우저가 아닌 웹서버에 저장하는 것입니다.
 
클라이언트의 정보를 웹브라우저에 저장하는 기술을 cookie라고 하죠. django의 session은 쿠키에는 sessionId만을 저장하여, 클라이언트와 웹서버간의 연결성을 확보한뒤 sessionId를 통해 커뮤니케이션을 실행합니다.
 
session의 라이프사이클은 브라우저에 의존합니다. 같은 브라우저를 사용하고 있다면 링크를 통해서 다른 사이트로 이동할때도 sessionId는 쿠키로써 쭉 유지되고, 브라우저를 닫으면 사라집니다.
 
cookie와 session의 구체적인 이해는 다음 링크를 참조하세요. 링크

session의 원리

  1. 유저가 웹사이트에 접속
  2. 웹사이트의 서버가 유저에게 sessionId를 부여
  3. 유저의 브라우저가 이 sessionId를 cookie에 보존
  4. 통신할때마다 sessionId를 웹서버에 전송(따라서 django의 경우 request객체에 sessionId가 들어있음)
  5. sessionId에 의해 웹사이트는 많은 접속 유저중 특정 유저를 인식할 수 있음

Cookie란

Cookie는 접속한 웹사이트에 의해 작성된 파일입니다. 사이트에서는 Cookie를 사용해 유저의 로그인 상태를 유지하거나 유저의 사이트 이용설정을 기억시킨다거나 유저의 지역 관련정보를 제공합니다.
 
웹서버에서 웹브라우저로 HTTP레스폰스의 헤더를 이용해서 작은 정보를 보냅니다. 이때 보내지는 정보를의 하나에 쿠키가 포함되어 있습니다. 쿠키는 서버 접속할때마다 자동전송됩니다. 물론 쿠키를 만든 웹사이트 이외에서는 다른 사이트가 만든 쿠키를 볼 수 없습니다.

Django소스코드로 session확인해보기

편의상 어플리케이션 urls.py, views.py, templates파일(index.html, result.html) 4개의 파일을 통해 session이 어떻게 활용되는지 알아보겠습니다.

urls.py

from django.contrib import admin
from django.urls import include, path
from . import views

app_name = 'sessiontest'
urlpatterns = [
    path('', views.index, name='index'),
    path('result/', views.result, name='result'),
]

views.py

from django.shortcuts import render
from formtest.forms import MyForm, FilterForm


def index(request):
    print(request.session.session_key)
    request.session['test'] = "hahaha"
    return render(request, 'sessiontest/index.html')


def result(request):
    session_id = request.session.session_key
    test = request.session['test']

    contents = {
        'session_id': session_id,
        'test': test
    }
    return render(request, 'sessiontest/result.html', contents)
  • index함수

    • 이때 처음 접속하는 브라우저에서는 request.session.session_key=None값을 찍어내게 됩니다.
    • request.session['test'] : test라는 키에 'hahaha'를 저장합니다. 이게 django의 session을 활용하면 좋은점인데요. sessionId 같은건 개발자가 고려하지 않고 request.session과 같은 간단한 커맨드로 session을 활용할 수 있습니다.
  • result함수

    • result페이지에서는 sessionId와 request.session['test']저장한 내용물을 꺼내어 html에 표시합니다.

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Thank You</title>
</head>
<body>
<h2>WELCOME</h2>


</body>
</html>

result.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Thank You</title>
</head>
<body>
    <h2>Response Entered by you:</h2>
    <p>
        sessionID: <strong>{{ session_id }}</strong>
    </p>
    <p>
        test: <strong>{{ test }}</strong>
    </p>

</body>
</html>

result.html 결과

  • d7v0bqwvagjq8fzcnjt76wgxb0s7zoc5라는 sessionId를 통해서 웹브라우저와 서버가 통신하는 것을 알 수 있습니다.

웹브라우저에서 정말 SessionId가 쿠키에 저장되어 있는지 확인해보기

chrome://settings/siteData에 접속하면 각 사이트가 가지고 있는 쿠키정보를 확인할 수 있습니다. 이번의 예에서 저는 localhost를 사용하였으므로 localhost의 쿠키정보를 까볼까요?
 


 
sessionId라는 키안에 d7v0bqwvagjq8fzcnjt76wgxb0s7zoc5 sessionId가 잘 저장되어 있습니다.

session의 라이프사이클 확인

이번에는 session의 라이프사이클을 확인하기위해 열려있던 브라우저를 모두 닫고 새로운 브라우저를 열어 sessionId가 바뀌었는지 어떤지 확인해보겠습니다.

잘 바뀌어 있네요.

들어가기 전에

django websocket 라이브러리 중 가장 신용도가 높은 channels 듀토리얼을 공부해봤습니다. 다음 링크에서 듀토리얼을 제공합니다 링크
듀토리얼을 따라하는데 체 30분이 걸리지 않으므로 꼭 손으로 코딩해주시길 바라요!
 
다음의 깃에 tutorial 코드를 업로드 했습니다. Django_ws_tutorial in Github  

듀토리얼 시켄스

아래의 시켄스는 다음과 같이 구분되어 있습니다.

  1. group을 사용하지 않는 단순 웹소켓
  2. group을 사용하는 웹소켓
     
    듀토리얼을 할 때, 흐름잡기에 도움이 되면 좋겠습니다.
     
     

+ Recent posts