React+Djangoでブログを作る1〈モデル定義編〉

WordPressはブログに必要な機能が一から十まで全部用意されていて、自分は記事を書いてテンプレートを選ぶ十一の部分だけやれば良いというお手軽感があります。ブログのカスタマイズは用意されたプラグインを必要なだけ導入すれば良いので、通常の利用では全く困る事が有りません。

しかしある程度触っていると、何処で何が行われているのかが分からないため、いざプラグインが存在しないような機能が欲しくなった時や、そもそもプラグインでは実現出来ないようなWordPressの根幹の部分が変更したくなった時に、モヤモヤする事が多くなりました。

プラグインで何とかなる場面は自分でプラグインを作れば良いのですが、そのような解決法ではプラグインを作る学習コストはともかく最終的にWordPressがプラグインでゴテゴテになってしまい、精神的・処理コスト的にあまり嬉しくはありません。

そもそも、プラグインではほとんどどうにもならないWordPressのデータ管理方法や記事の下書き等のシステム等色々な面で自分の好みとは違う点がある事に気付いたので、だったらいっその事自分の好みにあったブログシステムを自作してみようと思いました。

作りたい物

以下の機能を満たす自分用のブログシステムを作りたいと考えています。

  • 記事の表示
  • Markdownで記事が書ける
  • 記事の執筆
  • ブログテンプレートをある程度簡単に差し替え
  • SEO対策としてXMLサイトマップ生成やOGPタグの設定機能
  • マルチユーザー

構成

今回はウェブサイトの仕組みを学ぶ目的よりも、ブログシステムを作りたいという意思の方が強いため、一から四くらいまではフレームワークやライブラリに任せて、その先で目標のシステムを構築する事にします。

ウェブサイトについては完全に素人なので、自分が考えられる範囲で考えた以下の構成で構築を行います。

  • Django + Django REST Framework + CORS
  • React + Axios

簡単に説明すると、Djangoでバックエンドを実装して、Reactでフロントエンドを実装します。
その時、Django側はREST FrameworkとCORSを導入する事で、React側はAxiosを導入する事で双方の連携が取れるようにします。

Django : バックエンド

Djangoは、RailsやLaravelと並ぶPythonの有名なウェブフレームワークの一つです。

Django REST Framework

本来のDjangoはバックエンドとしてサーバー機能と、フロントエンドとして実際に表示する画面を生成する機能の両方を備えた総合的なフレームワークですが、今回はREST Frameworkを用いる事で、Djangoにはデータの管理と配信に徹してもらう事にします。

CORS

Cross-Origin Resource Sharingの略で、異なるドメイン間で動作するアプリケーションがリソースを共有するための仕組みだそうです。

今回はDjango(ポート8000番) + React(ポート3000番)で異なる2つのシステムが稼働しているため、これが必要になるようです。

React : フロントエンド

Reactは、Facebookによってオープンソースで開発されているJavaScriptのUI構築フレームワークです。

詳しい事は分かりませんが、良い感じのウェブ画面を作ってくれる物だと思っています。

今回は、Djangoから受け取ったデータを元にブログの画面を構成する役割を担ってもらいます。

Axios

ReactでREST APIへのリクエスト処理を実装するためのライブラリです。

これを使ってDjangoで用意したREST APIへアクセスし、必要なデータの取得や書き込みを行います。

Djangoでバックエンドを作る

早速作っていきます。

Djangoのインストール

まずはPythonにDjangoをインストールします。仮想環境等は好みで作成してください。

# バージョン確認
python -V
Python 3.7.4

# Djangoのインストール pip install django djangorestframework django-cors-headers

プロジェクトのセットアップ

きちんとPATHが通っていればDjangoのコマンドが利用出来るはずですので、それを使ってDjangoのプロジェクトを立ち上げます。
プロジェクト名は何でも良いですが、今回はWordPressに感化されたのでblogressと名付けます。

# プロジェクトの作成
django-admin startproject blogress
# 作成されたディレクトリに移動 cd blogress

以下のような内部構造を持つディレクトリblogressが生成されたと思います。
以降は、作成されたプロジェクトルートblogress/内で話を進めます。

blogress/ (以降何も表記しなければここがルートディレクトリ)
|-- blogress/
    |-- 省略...
    |-- settings.py
|-- manage.py

Djangoのプロジェクトは、アプリと呼ばれる複数の機能から成り立っています。

ユーザーモデルの変更

デフォルトのDjangoでは、ユーザー名とパスワードでログインするユーザーモデルが定義されていますが、まずはこれをメールアドレスとパスワードでログインする仕様に変更します。

まず、カスタムユーザーのアプリを追加します。Djangoでは、それぞれ異なる機能を持った複数のアプリからプロジェクトが成り立っているため、何か特定の機能を追加する場合にはまずアプリを追加します。

# アプリの追加
$ python manage.py startapp users

追加すると、プロジェクトルートにusers/というディレクトリが作成されたと思います。

blogress/
|-- blogress/
|-- users/ # 作成された
|-- manage.py

アプリを追加したので、まずはblogress/settings.pyにアプリを登録しましょう。
また、今から作るUserというカスタムユーザーモデルを使用するための設定も記述します。

# カスタムユーザーモデルの設定
AUTH_USER_MODEL = 'users.User' # 追記

INSTALLED_APPS = [
    # アプリを登録
    'users.apps.UsersConfig', # 追記

    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

アプリを登録したら、users/models.pyにモデルを定義します。

import uuid
from django.db import models
from django.core.mail import send_mail
from django.contrib.auth.models import PermissionsMixin
from django.contrib.auth.base_user import AbstractBaseUser
from django.utils.translation import ugettext_lazy as _
from django.utils import timezone
from django.contrib.auth.base_user import BaseUserManager


class UserManager(BaseUserManager):
    """ユーザーマネージャー."""

    use_in_migrations = True

    def _create_user(self, email, password, **extra_fields):
        """Create and save a user with the given username, email, and
        password."""
        if not email:
            raise ValueError('The given email must be set')
        email = self.normalize_email(email)

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

    def create_user(self, email, password=None, **extra_fields):
        extra_fields.setdefault('is_staff', False)
        extra_fields.setdefault('is_superuser', False)
        return self._create_user(email, password, **extra_fields)

    def create_superuser(self, email, password, **extra_fields):
        extra_fields.setdefault('is_staff', True)
        extra_fields.setdefault('is_superuser', True)

        if extra_fields.get('is_staff') is not True:
            raise ValueError('Superuser must have is_staff=True.')
        if extra_fields.get('is_superuser') is not True:
            raise ValueError('Superuser must have is_superuser=True.')

        return self._create_user(email, password, **extra_fields)


class User(AbstractBaseUser, PermissionsMixin):
    """カスタムユーザーモデル."""
    id = models.UUIDField(default=uuid.uuid4,
                            primary_key=True, editable=False)

    email = models.EmailField(_('email address'), unique=True)
    username = models.CharField(_('ニックネーム'), max_length=150, blank=True)

    is_staff = models.BooleanField(
        _('staff status'),
        default=False,
        help_text=_(
            'Designates whether the user can log into this admin site.'),
    )
    is_active = models.BooleanField(
        _('active'),
        default=True,
        help_text=_(
            'Designates whether this user should be treated as active. '
            'Unselect this instead of deleting accounts.'
        ),
    )
    date_joined = models.DateTimeField(_('date joined'), default=timezone.now)

    objects = UserManager()

    EMAIL_FIELD = 'email'
    USERNAME_FIELD = 'email'
    REQUIRED_FIELDS = []

    class Meta:
        verbose_name = _('user')
        verbose_name_plural = _('users')

    def get_full_name(self):
        full_name = '%s' % (self.username)
        return full_name.strip()

    def get_short_name(self):
        """Return the short name for the user."""
        return self.username

    def email_user(self, subject, message, from_email=None, **kwargs):
        """Send an email to this user."""
        send_mail(subject, message, from_email, [self.email], **kwargs)

Djangoの管理画面でもこのカスタムユーザーモデルを扱うために、users/admin.pyを以下の通り記述します。

from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from django.contrib.auth.forms import UserChangeForm, UserCreationForm
from django.utils.translation import ugettext_lazy as _
from .models import User


class MyUserChangeForm(UserChangeForm):
    class Meta:
        model = User
        fields = '__all__'


class MyUserCreationForm(UserCreationForm):
    class Meta:
        model = User
        fields = ('email',)


class MyUserAdmin(UserAdmin):
    fieldsets = (
        (None, {'fields': ('email', 'password')}),
        (_('Personal info'), {'fields': ('username',)}),
        (_('Permissions'), {'fields': ('is_active', 'is_staff', 'is_superuser',
                                       'groups', 'user_permissions')}),
        (_('Important dates'), {'fields': ('last_login', 'date_joined')}),
    )
    add_fieldsets = (
        (None, {
            'classes': ('wide',),
            'fields': ('email', 'password1', 'password2'),
        }),
    )
    form = MyUserChangeForm
    add_form = MyUserCreationForm
    list_display = ('email', 'username', 'is_staff')
    list_filter = ('is_staff', 'is_superuser', 'is_active', 'groups')
    search_fields = ('email', 'username')
    ordering = ('email',)

admin.site.register(User, MyUserAdmin)

これらの実装によって、Djangoのユーザー及び管理画面でのユーザーが定義したカスタムユーザーモデルに置き換えられます。

記事に関するアプリの追加

カスタムユーザーモデルを設定したら、今度は記事の投稿や表示を行うアプリpostsを追加しましょう。

# アプリの追加
$ python manage.py startapp posts

アプリを追加したので、blogress/settings.pyに登録しましょう。

INSTALLED_APPS = [
    # アプリを登録
    'users.apps.UsersConfig',
    'posts.apps.PostsConfig', # 追記

    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

次に記事のモデルをposts/models.pyに実装します。一般的なブログ記事が持つような情報を含む以下のようなモデルを作成しました。

import uuid
from django.db import models
from django.contrib.auth import get_user_model
User = get_user_model()


class Post(models.Model):
    # 記事ID(PRIMARY KEY)
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    # 著者
    author = models.ForeignKey(User, on_delete=models.CASCADE)
    # 記事タイトル
    title = models.CharField(max_length=64)
    # スラッグ(URLに使う記事識別子)
    slug = models.SlugField(unique=True)
    # サムネイルURL
    thumbnail = models.URLField(blank=True)
    # 記事本文
    body = models.TextField(blank=True)
    # 記事の作成日時と更新日時
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    def __str__(self):
        return self.title

記事の管理を出来るように管理者へ記事の管理権限を登録します。
posts/admin.pyに以下を記述してください。

from django.contrib import admin
from .models import Post

admin.site.register(Post)

ここまで実装したら、マイグレーションを行います。

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

データベースマイグレーション

マイグレーションとは、データベースの定義を自動的に管理する機能です。

Djangoでは新たなアプリを登録した際や、アプリの持つデータ構造が変わった際にこの操作が必要になります。

マイグレーションの情報が記載されたファイルを用意して順番に適用するので、そのファイルをもとにしたロールバック(変更を元に戻す)機能も付属しているみたいですが、今の所はよく分かっていません。

実際にマイグレーションを行う場合は、以下のコマンドを実行します。

# マイグレーションの情報を生成
python manage.py makemigrations
# マイグレーションの実行(IDは入力しなくても良い) python manage.py migrate マイグレーションID

管理者の作成

Djangoの管理パネルにログインするための管理者を作成します。
デフォルトではユーザー名、メールアドレス、パスワードを聞かれますが、カスタムユーザーモデルを適用しているのでメールアドレスとパスワードのみ登録します。

$ python manage.py createsuperuser
Email address: test@mail.com
Password:
Password (again):
# パスワードが単純過ぎたりすると聞かれる
# Bypass password validation and create user anyway? [y/N]: y
Superuser created successfully.

確認のため、管理コンソールへアクセスしてみましょう。

# Djangoサーバーの起動
$ python manage.py runserver
# http://localhost:8000/admin/ へアクセス

ちなみに、Djangoではサーバーを起動した状態でアプリ等のスクリプトに変更が有ると自動的に再読み込みを行うので、開発中にいちいちサーバーを落とす必要はありません。

Postsのデータの作成

管理画面からPostsのデータを作成してみます。

管理画面にまずログインします。

Postsの[+ Add]から、タイトルや本文のデータを入力して保存しましょう。

適当に項目を埋めてSAVEを押します。太字になっている項目は必須です。今回はthumbnailのみ空欄を許可しているので、それ以外の部分を埋める必要があります。

ひとまず3つほど記事を作成しました。

テストコードの記述

せっかく記事のモデルを作ったので、Djangoのテスト機能を使ってテストを行いましょう。

適当なユーザーがタイトル等諸々の情報を入力して投稿出来るかというテストです。

from django.test import TestCase
from django.contrib.auth import get_user_model

from .models import Post
User = get_user_model()

class BlogTests(TestCase):
    @classmethod
    def setUpTestData(cls):
        # Create a User
        testuser1 = User.objects.create_user(
            email='testuser1@example.com',
            password='abc123!'
        )
        testuser1.save()

        # Create a blog post
        test_post = Post.objects.create(
            author=testuser1,
            title='Blog title',
            slug='test-post1',
            thumbnail='https://test.com/dog.jpg',
            body='Body content...'
        )
        test_post.save()

    def test_blog_content(self):
        post = Post.objects.get()
        expected_author = f'{post.author}'
        expected_title = f'{post.title}'
        expected_slug = f'{post.slug}'
        expected_thumbnail = f'{post.thumbnail}'
        expected_body = f'{post.body}'
        self.assertEquals(expected_author, 'testuser1@example.com')
        self.assertEquals(expected_title, 'Blog title')
        self.assertEquals(expected_slug, 'test-post1')
        self.assertEquals(expected_thumbnail, 'https://test.com/dog.jpg')
        self.assertEquals(expected_body, 'Body content...')

書き終えたらCtrl + Cでいったんサーバーを終了して以下のコマンドで実行します。

$ python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.
----------------------------------------------------------------------
Ran 1 test in 0.117s

OK
Destroying test database for alias 'default'...

どうやら上手くテストが通ったようです。

まとめ

Djangoのインストールから、ブログシステムの基本となる記事の管理の基礎を作成しました。

Djangoはフルスタックなフレームワークなので覚える事が多いですが、徐々に慣れていきたいです。

参考

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です