Djangoを使って簡単な投稿サイトを作成

Django
Lawrence MonkによるPixabayからの画像

フレームワークのこととか勉強するときは実際になにか作るのが良いと聞きます。

そこで今回は簡単な記事作成webアプリを作ってみました。

今回だけでなく何回かに分けてこのアプリに機能を追加していきつつDjangoのことを学んでいきたいと思います。

環境

  • OS: Windows10 home 64bit
  • フレームワーク: Django 3.1.2
  • 言語: Python 3.9
  • エディター: VSCode

今回実装したいもの

  • 自分が作成した記事一覧画面
  • 記事の新規作成画面
  • 特定の作成された記事を表示する画面

手を加えるファイル

fetch_site/
  ├ fetch_apps/
  │   ├ templates/
  │   │    └ fetch_apps/
  │   │         ├ create.html
  │   │         ├ list.html
  │   │         └ preview.html
  │   ├ admin.py
  │   ├ forms.py
  │   ├ models.py
  │   ├ urls.py
  │   └ views.py
  └ fetch_site/
      ├ settings.py
      └ urls.py

Djangoのテンプレートを用意する

Python, Djangoは既にインストールされていることを前提として話を進めていきます。

まずはwebアプリのひな型を用意してそこから新しい機能を実装していきたいのですが、
Djangoでは二回コマンドを実行することで直ぐに用意できます。

まず、一番大きな枠組みのfetchサイトを以下のコマンドで作成します。

django-admin startproject fetch_site

作った後、プロジェクト内に今回の本命となるアプリのひな型を作ります。

cd fetch_site
python manage.py startapp fetch_apps

これでとりあえずひな型はできました。

ここから記事アプリを実装していきます。

アプリの実装

ここまでの作業だけだとfetch_site自身がfetch_appsのアプリを持っていることを認識していません。
なのでfetch_siteのsettingsにfetch_appsを登録します。

settings.pyINSTALLED_APPSに以下のように追加します。

# fetch_site/fetch_site/settings.py
INSTALLED_APPS = [
    'fetch_apps.apps.FetchAppsConfig',
~~~~~~~~~~~~ 省略 ~~~~~~~~~~~~~~~~~~~~~
]

また、今後の実装のことを考えてfetch_site/fetch_site/urls.pyにfetch_appsのurlを登録しておきます。

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

urlpatterns = [
    path('', include('fetch_apps.urls')),  # ...1
    path('admin/', admin.site.urls),
]
  • 1: webアプリでは新しい画面を追加するときにその画面に遷移するURLを追加する場面が多々あるかと思います。
    そのたびにfetch_siteのurls.pyに追加していると他のアプリを作成した場合など管理が大変になる可能性があります。
    なので今回実装するアプリのメインページのurlだけを登録することにします。

データの用意

作成する記事には「タイトル」「本文」「カテゴリ」「タグ」の四つのデータを持たせようと思います。

Djangoではこれらのデータを持ったテーブルをモデルとしてクラス化し、プログラム内で扱っていきます。

ということでfetch_appsフォルダの中にmodels.pyのファイルがあるはずなので実装していきましょう。

# fetch_site/fetch_apps/models.py
from django.db import models


class Tag(models.Model):
    name = models.CharField(max_length=20)

    def __str__(self):  # ...1
        return self.name


class Article(models.Model):
    title = models.CharField(max_length=20)
    text = models.TextField(max_length=100, null=True, blank=True)
    date = models.DateField(auto_now=True)
    category = models.ForeignKey('Category', on_delete=models.CASCADE, null=True, blank=True)  # ...2
    tag = models.ManyToManyField(Tag, blank=True)  # ...3
    del_flag = models.BooleanField(default=False)  # ...4

    def __str__(self):
        return self.title


class Category(models.Model):
    name = models.CharField(max_length=20)

    def __str__(self):
        return self.name
  • 1: Tag, Article, Categoryの各モデルに__str__メソッドを実装います。
    このことでadmin画面の各モデルの一覧に表示されるデータの識別がしやすくなります。
  • 2: それぞれの記事をカテゴリごとに分けて管理できるようにcategoryの項目を追加しました。
    利便性を考えてCategoryモデルに必要なカテゴリを複数用意し、Articleモデルのデータと紐づけできるようにForeignKeyを使いました。
  • 3: TagモデルもCategoryと同様にタグを事前に用意しておいて必要に応じてArticleデータと紐づけできるようにする。
    今回は複数のTagをArticleと紐づけできるようにしたいのでManyToMantyを使うことにしました。
  • 4: このフラグは作成した記事を削除しても復旧できるようにするための削除フラグです。
    今回は実装しませんが、後に一回目の削除でゴミ箱に移動し、ゴミ箱から再度削除すると完全に削除できる機能を実装できるように一応準備しておきます。

さらに上で用意したモデルのデータ管理をしやすくするためにadmin画面に表示できるようにします。

そのためにfetch_apps/admin.pyのファイルに以下のようにコードを追加します。

# fetch_site/fetch_apps/admin.py
from django.contrib import admin

from fetch_apps.models import Tag, Article, Category


admin.site.register(Tag)
admin.site.register(Article)
admin.site.register(Category)

これでとりあえず今回必要なモデルは用意できたのでデータベースに反映していきます。

まずカレントディレクトリがfetch_siteになっているかを確認します。

下のコマンドでマイグレーションを作成します。

python manage.py makemigrations

するとmigrationsフォルダに新しいマイグレーションファイルが作成されます。
これを以下のコマンドを実行することでデータベースに反映させることができるようになります。

python manage.py migrate

adminの画面で作業ができるように専用のユーザーを以下のコマンドで作っておきます。

python manage.py createsuperuser

http://127.0.0.1:8000/adminに設定したユーザーのアカウントでログインをして
Category, Tagのモデルにデータをいくつか入れて置きます。

記事作成画面

現時点では記事に関するデータがないため一覧画面にも完成記事の表示画面も表示することができません。

なのでまずは記事を作成する機能を実装していきます。

from django.forms import ModelForm, TextInput, Textarea

from fetch_apps.models import Article


class ArticleForm(ModelForm):
    class Meta:
        model = Article  # ...1
        fields = ['title', 'text', 'category', 'tag']  # ...2
        widgets = {
            'title': TextInput(attrs={'placeholder': 'title'}),  # ...3
            'text': Textarea(attrs={'placeholder': 'text'})  # ...4
        }

    def save(self):
        article = Article.objects.create(
            title=self.cleaned_data['title'],
            text=self.cleaned_data['text'],
            category=self.cleaned_data['category']
        )
        if 'tag' in self.cleaned_data:
            article.tag.set(self.cleaned_data['tag'].values_list('id', flat=True))  # ...5
  • 1: Articleモデルを元に記事のタイトル、本文等を入力するフォームが生成されます。
  • 2: Articleモデル内で設定したフィールドの内、フォームとして表示したいフィールドを指定しています。
    ユーザーが直接入れるフィールドはタイトル、本文、カテゴリ、タグだけなのでこの四つだけを指定しています。
  • 3: デフォルトではフォーム内は空白なのでユーザーから見ると何を入力するフォームなのかわからなくなります。
    そのためフォーム中に文字を入れて分かりやすくするためにwidgetsの機能を使ってplaceholderを設定します。
  • 4: 3と同じようにplaceholderを設定しているのですが、Articleモデルで設定したTextFieldにより生成されるフォームはそのまま使うと一行分のフォームしか表示されませんでした。
    そこでTextareaをwidgets内で設定することでhtmlのtextareaタグを設定したのと同じようになります。
  • 5: ArticleモデルのtagフィールドとTagモデルとはManytoManyで紐づけられています。
    入力されたタグのフォームからidをリスト形式で受け取りそのままtagフィールドに保存しています。
# fetch_site/fetch_apps/views.py
from django.shortcuts import render

from fetch_apps.forms import ArticleForm


def create_new_article(request):
    if request.method == 'POST':
        form = ArticleForm(request.POST)  # ...1
        if form.is_valid():  # ...2
            form.save()
            return render(request, 'fetch_apps/create.html', {'form': form})  # ...3

    return render(request, 'fetch_apps/create.html', {'form': ArticleForm()})
  • 1: ユーザーが入力したデータをそのまま上で用意したフォームに渡すことでフォームにデータを入力した状態を維持して作成画面を開いたり、そのままデータに保存することもできるようになります。
  • 2: モデル内で設定したフィールドにオプションを色々設定したかと思います。
    ここではバリデーションをかけることでそのオプションを満たしているかどうかを判定してくれます。
    フィールドとしての条件を満たしていれば、そのあとのform.save()が実行されデータが保存されます。
  • 3: 今回は作成が完了したら入力したデータをフォームに残したまま作成画面に戻ってくるようにしました。
<!-- fetch_site/fetch_apps/templates/fetch_apps/create.html -->
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>記事作成画面</title>
  </head>
  <body>
    <h1>記事作成画面</h1>
    <form action="/create/" method="post">
      {% csrf_token %}
      {{form.title}}
      <br>
      {{form.text}}
      <br>
      新規作成日: {% now "Y年 n月 j日" %}  <--  1
      <br>
      <label>カテゴリ:{{form.category}}</label>  <-- 2
      <br>
      <label>タグ:{{form.tag}}</label>  <-- 3
      <br>
      <input type="submit" value="Submit">
    </form>
  </body>
</html>
  • 1: 最初はviews.py側で作成日の日付を取得してhtml画面に表示しようと考えていました。
    しかしDjangoの組み込みテンプレートタグにnowという便利なものがあったのでそれを使うことにしました。(詳しくは下のリンク)
    Articleモデルのdateフィールドはオプションで特に指定しなければデータを作成した日付が自動的に保存される仕様になっているのでとりあえずは問題ないと思います。
  • 2, 3: カテゴリとタグはformからただcategory, tagのフィールド名を指定して表示するだけでプルダウン形式または複数選択できる形式のフォームが自動で表示されます。
    これはかなり便利!
# fetch_site/fetch_apps/urls.py
from django.urls import path

from fetch_apps import views


urlpatterns = [
    path('create/', views.create_new_article, name='create'),  # ...1
]
  • 1: この行を加えることでhttp://127.0.0.1:8000/createにアクセスしたときにviews.pyのcreate_new_articleを参照するようになります。

これで記事作成画面はできました。

python manage.py runserverを実行し、http://127.0.0.1:8000/createにアクセスしてみると
以下の画面が表示されます。

実際にデータを入力してsubmitボタンを押すと入力したデータがデータベースに保存されます。
記事を作成する前と後で画面が変わらないので本当にデータが作られているか分からないですね・・・

しかしhttp://127.0.0.1:8000/adminにアクセスしてArticlesを開いてみると先ほど作成したデータが保存されていることが分かります。

記事一覧画面

次は作成した記事を一覧表示する処理を実装します。

# fetch_site/fetch_apps/views.py
from django.shortcuts import render

from fetch_apps.forms import ArticleForm
from fetch_apps.models import Article, Category


def list_up_articles(request):
    if 'POST' in request.method:
        if 'category' in request.POST and request.POST['category']:
            articles = Article.objects.filter(category__name=request.POST['category']).values('id', 'title', 'date')  # ...1
        else:
            articles = Article.objects.values('id', 'title', 'date')
    else:
        articles = Article.objects.values('id', 'title', 'date')
    categories = Category.objects.values('name')  # ...2

    return render(request, 'fetch_apps/list.html', {'categories': categories, 'articles': articles})  # ...3


def create_new_article(request):
~~~~~~~~~~~~~~~~~ 省略 ~~~~~~~~~~~~~~~~~~~~~~~~~~
  • 1: 一覧画面でカテゴリに関してフィルターをかけた場合、その条件に該当するAritcleデータだけを取得するようにしています。
    また、Articleのデータ全てを取得する必要はないのでここでは特定のフィールドのみ取得するように限定しています。
    2: カテゴリのフィルターを表示するためにCategoryのnameフィールドを取得しています。
  • 3: フィルター用のカテゴリデータと条件に合ったArticleデータを一覧画面に渡しています。
<!-- fetch_site/fetch_apps/templates/fetch_apps/list.html -->
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>記事一覧画面</title>
  </head>
  <body>
    <h1>記事一覧画面</h1>
    <a href="{% url 'create' %}">記事新規作成</a>  <-- 1
    <form action="/" method="post">
      {% csrf_token %}
      カテゴリ:
      <select name="category">
        <option value="">-----------</option>  <-- 2
        {% for category in categories %}
        <option value="{{category.name}}">{{category.name}}</option>  <-- 3
        {% endfor %}
      </select>
      <br>
      <input type="submit" value="Submit">
    </form>
    <ul>
      {% for article in articles %}
      <li>
        {{article.title}} 更新日: {{ article.date|date:"Y" }}年 {{ article.date|date:"n"}}月 {{ article.date|date:"j"}}日  <-- 4
      </li>
      {% endfor %}
    </ul>
  </body>
</html>
  • 1: 一覧画面から記事の新規作成画面に遷移できるようにurlを設定しています。
    正確にはurlのテンプレートタグを使ってurls.pyに設定した作成画面のurlのnameの値を指定しています。
  • 2: 作成画面のときとは違いCategoryモデルから取得してきたデータをforループして表示しているので非選択状態の”——“を自分で用意しなくてはいけません。
    なのでここではhtml内で直接用意しています。
    (もしかしたら非選択の項目もDjangoの機能を使ってできるかもしれません。)
  • 3: viewsから渡したカテゴリのデータをプルダウンのフォームに設定しています。
  • 4: 一覧画面に表示する各記事の値はタイトルと作成された日付なのでforループで並べています。
    日付を表示は上のソースコードで示している以外にも種類があるので公式のページで探してみてください。
# fetch_site/fetch_apps/urls.py
from django.urls import path

from fetch_apps import views


urlpatterns = [
    path('', views.list_up_articles, name='index'),
    path('create/', views.create_new_article, name='create'),
]

一覧画面をメインのページに設定しました。

これでhttp://127.0.0.1:8000/にアクセスすると一覧画面が以下のように表示されます。

完成した記事を表示する画面

作成した記事の完成の状態を見れる画面を実装します。

プレビューの画面としても使っていいかもしれません。

# fetch_site/fetch_apps/views.py
from django.shortcuts import render

from fetch_apps.forms import ArticleForm
from fetch_apps.models import Article, Category


def list_up_articles(request):
~~~~~~~~~~~~~ 省略 ~~~~~~~~~~~~~~~


def create_new_article(request):
~~~~~~~~~~~~~ 省略 ~~~~~~~~~~~~~~~


def view_article(request, id):
    return render(request, 'fetch_apps/view_article.html', {'article': Article.objects.get(id=id)})  # ...1
  • 1: パラメータに指定されたidの記事を取得してhtml側に渡しています。
<!-- fetch_site/fetch_apps/templates/fetch_apps/view_article.html -->
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>記事表示</title>
  </head>
  <body>
    <h1>出来上がった記事画面</h1>
    {{article.title}}
    <br>
    {{article.text}}
    <br>
    更新日: {{ article.date|date:"Y" }}年 {{ article.date|date:"n"}}月 {{ article.date|date:"j"}}日
    <br>
    <label>カテゴリ:{{article.category}}</label>
    <br>
    <label>タグ:
      {% for tag in article.tag.all %}  <-- 1
        {{tag}}
      {% endfor %}
    </label>
  </body>
</html>
  • 1: ManyToManyで紐づいたtagをどうやってhtml側で取得できるんだろうと思っていましたが意外とpython側の書き方と同じような書き方でできました。
    シンプルで良き・・・
# fetch_site/fetch_apps/urls.py
from django.urls import path

from fetch_apps import views


urlpatterns = [
    path('', views.list_up_articles, name='index'),
    path('create/', views.create_new_article, name='create'),
    path('view_article/<int:id>/', views.view_article, name='view_article'),
]

ここはDjangoの「URL ディスパッチャ」を参考に追加

<!-- fetch_site/fetch_apps/templates/fetch_apps/list.html -->
~~~~~~~~~~~~~ 省略 ~~~~~~~~~~~~~~~~~~~
    <ul>
      {% for article in articles %}
      <li>
        <a href="{% url 'view_article' article.id %}">  <-- 1
          {{article.title}} 更新日: {{ article.date|date:"Y" }}年 {{ article.date|date:"n"}}月 {{ article.date|date:"j"}}日  <-- 4
        </a>
      </li>
      {% endfor %}
    </ul>
  </body>
</html>
  • 1: 一覧画面から各データをクリックすることで各記事の完成画面を表示できるようにリンクを追加

http://127.0.0.1:8000/を開き一覧画面の各記事のリンクをクリックすることで下画像のように完成記事が表示されます。

まあかなりしょぼい感じがしますがデザインは後々時間のあるときに・・・

最後に

これでDjangoを使った記事作成サイトがとりあえず出来ました。

今後さらに機能を実装してデザインも綺麗にしていきたいと考えてます。

なにかしら参考になれば幸いです

ここまで読んでくれてありがとう!!!!

コメント

タイトルとURLをコピーしました