LoginSignup
9
14

WEBアプリの勉強を兼ねてDjangoで備忘録登録アプリを作ってみる

Last updated at Posted at 2019-05-06

◇この記事について

今回、初めての投稿となります(ホントは平成の間に初投稿といきたかったのですが、よくある日程遅延で令和までもつれ込みました・・)。
私自身プログラミング経験はあったんですが、WEBアプリ関係の知識がほとんどなかったため、知識・スキルを広げることを目的にこのアプリを作成してみました。
まずは、このアプリをベースにWEBアプリケーションに関する知識を少しずつ深めていければと思います。

◇記事投稿順(2019/06/29追記)

今回は、ベースのWEBアプリを1.の記事で作成し、2.以降の記事で機能の追加や改善を行っています。必要に応じてほかの記事も参照ください。

  1. 【本記事】WEBアプリの勉強を兼ねてDjangoで備忘録登録アプリを作ってみる
  2. Djangoで作った備忘録登録WEBアプリの高機能化①(タイトル・タグ・本文検索機能の付加)
  3. Djangoで作った備忘録登録WEBアプリの高機能化②(タグ一覧表示、タグ別記事の追加など)

◇開発環境

  • OS : Ubuntu 18.04.2 LTS(Windows Subsystem for Linux)
  • 言語 : Python 3.6.7
  • Webアプリフレームワーク : Django (2.2)
  • DB : SQLite3

◇事前知識

◇実装内容

◆初期設定関係

Djangoのインストール手順は、はじめての Django アプリ作成に沿って進めていきました。細かい部分は省略しますが、以下のコマンドで、プロジェクトおよび備忘録用アプリを新規作成します。

Terminal
$ django-admin startproject mysite
$ python manage.py startapp memorandum

次に、自動作成されたファイルの中にあるsettings.pyを修正します。具体的には、

  • INSTALLED_APPS内へのmemorandum.apps.MemorandumConfigの追加
  • LANGUAGE_CODEjaに修正
  • TIME_ZONEAsia/Tokyoに修正

を行っています。

mysite/mysite/settings.py

------

# Application definition

INSTALLED_APPS = [
    'memorandum.apps.MemorandumConfig',
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

------

# Internationalization
# https://docs.djangoproject.com/en/2.1/topics/i18n/

# LANGUAGE_CODE = 'en-us'
LANGUAGE_CODE = 'ja'

# TIME_ZONE = 'UTC'
TIME_ZONE = 'Asia/Tokyo'

USE_I18N = True

USE_L10N = True

USE_TZ = True

------

  • INSTALLED_APPS内へのmemorandum.apps.MemorandumConfigの追加

ここでは、mysite/memorandum/apps.py内にあるclass MemorandumConfigをプロジェクトに登録しています。
コード中にもあるように、name = 'memorandum'としているため、この名前(memorandum)で登録しても問題なさそう(実際、そうやっている説明サイトもある)でしたが、今回は参考にしたドキュメントに倣ってクラス名自体を記述しています。

mysite/memorandum/apps.py

from django.apps import AppConfig


class MemorandumConfig(AppConfig):
    name = 'memorandum'

  • TIME_ZONEAsia/Tokyoに修正

については、最初は修正をしてなかったんですが、一通りアプリができた後の検証作業でデータの追加日時がずれるという問題が発生したため、原因を調査して追加で修正しました。
この修正を行うことによって、日時が日本時刻で表示されるようになります。
USE_TZ = Trueをしておくと、内部的に保存されるデータはUTCで保存される仕様のようです。

USE_TZ = True
TIME_ZONE = 'Asia/Tokyo'

この設定の組み合わせで、

  • 内部で保存されるデータはUTC
  • 表示される日時データは日本時刻に自動変換される

という形になります。

参考URLDjangoで、タイムゾーンの変換

◆ファイル構成

treeコマンド実行時のターミナル出力結果(__pycache__下のファイルなどは削ってます)を下に載せます。自分で明示的に追加・修正した部分については、<-でコメントを追記してます。

Terminal
$ tree mysite/
mysite/
├── db.sqlite3
├── manage.py
├── memorandum
│   ├── __init__.py
│   ├── __pycache__
│   ├── admin.py
│   ├── apps.py
│   ├── migrations
│   │   ├── 0001_initial.py
│   │   ├── __init__.py
│   │   └── __pycache__
│   ├── models.py                    <-データベース内容定義等
│   ├── static
│   │   └── memorandum
│   │       └── css
│   │           └── memorandum.css   <-スタイルシート用ファイル
│   ├── templates
│   │   └── memorandum
│   │       ├── base-layout.html     <-レイアウト用ファイル
│   │       ├── create-tag.html      <-新規タグ作成ページ
│   │       ├── create.html          <-新規記事作成ページ
│   │       ├── delete.html          <-記事削除ページ
│   │       ├── detail.html          <-記事詳細表示ページ
│   │       ├── index.html           <-トップページ(記事一覧表示)
│   │       └── update.html          <-記事内容修正ページ
│   ├── tests.py
│   ├── urls.py                      <-memorandum内のルーティング定義等
│   └── views.py                     <-各関数のメイン処理定義等
└── mysite
    ├── __init__.py
    ├── __pycache__
    ├── settings.py                  <-言語設定、タイムゾーン設定等の定義(前述のとおり)
    ├── urls.py                      <-mysiteプロジェクト全体のルーティング定義等
    └── wsgi.py

◆データベースのモデル定義(models.py)

モデルの定義として、ArticleクラスとTagクラスの2つに分けて作成しました。
Tagクラスを分けたのは、一つの記事に複数のタグが登録できるようにしたいという考えからです。
Tagクラスでは、タグ名(name)のみを保存し、
メインのデータベースとなるArticleクラスでは、

  • タイトル(title
  • タグ名(tag
  • 本文(body
  • 作成日(published_date
  • 更新日(modified_date

を定義し、タグ名(tag)については、TagクラスをManyToManyFieldを付けて参照しています。
ManyToManyField を付けることによって、多対多の関係で紐づけが可能になります。

参考URLDjango2.0  ManyToMany(多対多)リレーションを使う

また、作成日(published_date)はauto_now_add=True、更新日(modified_date)はauto_now=Trueのオプションを付けています。

  • auto_now_add=True:データが新規作成される際に、その時の日付が自動で設定されます。
  • auto_now=True:データが新規作成または__更新__される際に、その時の日付が自動で設定されます。

これらのオプションを付けておくと、新規記事作成ページなどで作成日(published_date)と更新日(modified_date)のフォームを追加せずに自動で値が設定されます。

参考URLモデルフィールドリファレンス

models.py
from django.db import models

# Create your models here.
class Tag(models.Model):
    name = models.CharField(max_length=128, unique=True)

    def __str__(self):
        return self.name

class Article(models.Model):
    title = models.CharField(max_length=255)
    tag = models.ManyToManyField(Tag)
    body = models.TextField()
    published_date = models.DateField(auto_now_add=True)
    modified_date = models.DateField(auto_now=True)
    
    #これ入れないと、リスト一覧でタイトル名が表示されない
    def __str__(self):
        return self.title

models.pyの修正が終わったら、以下コマンドを実行し、修正したモデル定義をプロジェクトに反映させます。

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

◆URLルーティングの定義(urls.py)

まず、mysite下のurls.pyで、全体のURLルーティングを設定します。

mysite/urls.py
"""mysite URL Configuration

The `urlpatterns` list routes URLs to views. For more information please see:
    https://docs.djangoproject.com/en/2.1/topics/http/urls/
Examples:
Function views
    1. Add an import:  from my_app import views
    2. Add a URL to urlpatterns:  path('', views.home, name='home')
Class-based views
    1. Add an import:  from other_app.views import Home
    2. Add a URL to urlpatterns:  path('', Home.as_view(), name='home')
Including another URLconf
    1. Import the include() function: from django.urls import include, path
    2. Add a URL to urlpatterns:  path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import include, path

urlpatterns = [
    path('memorandum/', include('memorandum.urls')),
    path('admin/', admin.site.urls),
]

urlpatternspath('memorandum/', include('memorandum.urls')),を追加することにより、WEBアプリのmemorandum/下にアクセスが来た場合、memorandum下のurls.pyのルールが適用されるようになります。

次に、memorandum/のURLルーティングを設定します。

memorandum/urls.py
from django.urls import path

from . import views

app_name = "memorandum"
urlpatterns = [
    # path("", views.index, name="index"),
    path("", views.ArticleListView.as_view(), name="index"),
    path("create/", views.ArticleCreateView.as_view(), name="create"),
    path("<int:pk>/detail/", views.ArticleDetailView.as_view(), name="detail"),
    path("<int:pk>/update/", views.ArticleUpdateView.as_view(), name="update"),
    path("<int:pk>/delete/", views.ArticleDeleteView.as_view(), name="delete"),
    path("tag/create/", views.TagCreateView.as_view(), name="create_tag"),
]

この記載をすることにより、例えば、
memorandum/create/にアクセスがあった場合、views.py内のArticleCreateView.as_view()関数が呼び出されるといった動作になります。
name="create"で名前を付けておくことによって、HTML内のhrefなどでその名前を使ってリンクさせることが可能になります。

◆ビュー関数の定義(views.py)

views.pyで、それぞれのページアクセス時にどのデータを表示するか記述していきます。
ちなみに、今回はDjangoでサポートしているクラスベースビューというものを使用しています。
※このクラスベースビュー機能は、ListViewやCreateViewなど汎用的なビュークラスについてあらかじめDjnagoで定義されており、これらのクラスを使うことにより、実装作業を効率化できるというものです。

参考URLDjangoにおけるクラスベース汎用ビューの入門と使い方サンプル

views.py
from django.shortcuts import render,redirect
from django.urls import reverse_lazy
from django.http import HttpResponse
from django.views import generic

from .models import Tag, Article 

class ArticleListView(generic.ListView):
    model = Article

    #参照するhtmlファイルを指定
    template_name = "memorandum/index.html"

class ArticleDetailView(generic.DetailView):
    model = Article
    #参照するhtmlファイルを指定
    template_name = "memorandum/detail.html"

class ArticleCreateView(generic.edit.CreateView):
    model = Article
    fields = ["title","tag","body"]
    #参照するhtmlファイルを指定
    template_name = "memorandum/create.html"
    success_url = reverse_lazy('memorandum:index')  #POSTが正しく行われた際に飛ばすURL

class ArticleUpdateView(generic.edit.UpdateView):
    model = Article
    fields = ["title","tag","body"]
    #参照するhtmlファイルを指定
    template_name = "memorandum/update.html"
    success_url = reverse_lazy('memorandum:index')  #POSTが正しく行われた際に飛ばすURL

class ArticleDeleteView(generic.edit.DeleteView):
    model = Article
    #参照するhtmlファイルを指定
    template_name = "memorandum/delete.html"
    success_url = reverse_lazy('memorandum:index')  #POSTが正しく行われた際に飛ばすURL

class TagCreateView(generic.edit.CreateView):
    model = Tag
    fields = ["name"]
    #参照するhtmlファイルを指定
    template_name = "memorandum/create-tag.html"
    success_url = reverse_lazy('memorandum:index')  #POSTが正しく行われた際に飛ばすURL

それぞれのクラスの最初のmodel = Articleormodel = Tagで使用するモデル定義を選択しており、最低限、これだけでリスト等の表示が行えますが、必要に応じてデフォルト設定から書き換えたい部分については、書き換えていく形になります。

  • template_nameはページアクセス時に返すhtmlファイルの__ローカル側__のパスになります(デフォルトではクラス名_list.htmlのような名前のhtmlファイルを探してしまうようなので、基本的に明示的に設定してあげた方がいいかと思います)。
  • success_urlUpdateViewCreateViewの際に使用されるもので、保存実行時に正常に終了した場合に遷移させるページのURLになるようです。
  • reverse_lazyは、'memorandum:index'などで定義したルーティング情報をURLに変換する関数のようです(完全に理解できてない・・)。

    参考サイトを読んだ限りでは、success_urlで設定する場合、reverse関数ではなく、reverse_lazy関数を使う必要があるとのことだったので、それに倣っています。

参考URL[Django] success_urlとget_success_urlおよびreverseとreverse_lazyの使い分け

◆テンプレート(HTMLファイル)の作成

最後に、HTMLファイルとCSSファイルの作成を行います。全てのHTMLページを載せると記事が冗長になるため、いくつかかいつまんで載せていきます。

base-layout.htmlの作成

ページタイトル、ロゴ画面については、共通テンプレートを使用して記述しています。

base-layout.html
{% load static %}
<!DOCTYPE html>
<html lang="ja">
  <head>
    <link rel="stylesheet" href="{% static 'memorandum/css/memorandum.css' %}">
    <title>{% block title %}Memo Site{% endblock title %}</title>
    {% block head %}{% endblock head %}
  </head>

  <body>
    <header>
      <div class="top-title">
        <a class="home-link-text" href="{% url 'memorandum:index' %}">備忘録登録サイト</a>
      </div>
      {% block header %}
      {% endblock header %}
    </header>
    <div class="main-content">
      {% block content %}
      {% endblock content %}
    </div>
  </body>
</html>

□トップページ(記事一覧表示)index.htmlの作成

object_list内にモデル定義したデータが入っているため、forループで中のアイテムを一つづつ取り出し、そのデータを表示しています。
それぞれの要素については、models.pyで定義した変数名(titleなど)を{{ item.title }}という形で指定すればそのデータが表示されます。

index.html
{% extends 'memorandum/base-layout.html' %}

{% block content %}
  <div class="local-nav">
    <ul class="local-nav-list">
      <li class="local-nav-item"><a class="local-nav-link-text item-normal" href="{% url 'memorandum:create' %}">新規記事</a></li>
      <li class="local-nav-item"><a class="local-nav-link-text item-normal" href="{% url 'memorandum:create_tag' %}">新規TAG</a></li>
    </ul>
  </div>
  <ul class="article-list">
    {% for item in object_list %}
      <li class="article-list-item">
        <a class="link-text" href="{% url 'memorandum:detail' item.pk %}">{{ item.title }}</a>
        <div class="article-date">作成日: {{ item.published_date|date:"Y年m月d日(D)" }}<br>更新日: {{ item.modified_date|date:"Y年m月d日(D)" }}</div>
      </li>
    {% endfor %}
  </ul>
{% endblock content %}

index.png

□記事詳細表示ページdetail.htmlの作成

基本的には、index.html同様、{{ object.title }}といった形で表示したいデータを指定します。
今回は、__一つ__の記事の詳細を表示する形なのでforループは不要です。
また、{{ object.body | linebreaks }}のように、linebreaksを入れることにより、フォームで入力した改行コードがHTML表示に反映されます。

参考URLDjango: 組み込みタグとフィルタの一覧

detail.html
{% extends 'memorandum/base-layout.html' %}

{% block content %}
  <div class="local-nav">
    <ul class="local-nav-list">
      <li class="local-nav-item"><a class="local-nav-link-text item-normal" href="{% url 'memorandum:update' object.pk %}">編集</a></li>
      <li class="local-nav-item"><a class="local-nav-link-text item-caution" href="{% url 'memorandum:delete' object.pk %}">削除</a></li>
    </ul>
  </div>
  <div class="main-article">
    <h1 class="article-title">{{ object.title }}</h3>
    <ul class="article-tag-group">
      タグ:
      {% for tag in object.tag.all %}
        <li>
          <div class="article-tag">{{ tag }}</div>
        </li>
      {% endfor %}
    </ul>
    <div class="article-date">
      <span class="">作成日: {{ object.published_date|date:"Y年m月d日(D)" }}</span>
      <span class="">更新日: {{ object.modified_date|date:"Y年m月d日(D)" }}</span>
    </div>
    <p class="article-body">{{ object.body | linebreaks }}</p>
  </div>
{% endblock content %}

detail.png

□新規記事作成ページcreate.htmlの作成

create.htmlでは、フォームを使用してデータの追加をしていきます。
{{ form.title }}という形でタイトル用の入力フォームを表示できます。
また、{{ form.title.label_tag }}というようにlabel_tag を付けると、その要素の名前が表示されます。

create.html
{% extends 'memorandum/base-layout.html' %}

{% block content %}
  <div class="main-article">
    <form method="post">{% csrf_token %}
        <div class="input-form">
          <div class="input-form-position partition-bottom-line">
            <span class="input-label-text">{{ form.title.label_tag }}</span>
            <div class="input-form-title">{{ form.title }}</div>
          </div>
          <div class="input-form-tag-group partition-bottom-line">
            <div class="input-form-position">
              <span class="input-label-text">{{ form.tag.label_tag }}</span>
              <div class="input-form-tag">{{ form.tag }}</div>
            </div>
            <a class="update-inner-link-text item-normal" href="{% url 'memorandum:create_tag' %}">Add New TAG</a>
          </div>
          <div class="input-form-position partition-bottom-line">
            <span class="input-label-text">{{ form.body.label_tag }}</span>
            <div class="input-form-body">{{ form.body }}</div>
          </div>
        </div>
        <!-- {{ form.as_p }} -->
        <input class="button-common" type="submit" value="Save">
    </form>
  </div>
{% endblock content %}

create.png

update、deleteについては今回は割愛します。
ここまでの作業で備忘録の新規追加、一覧表示、詳細表示、記事修正、削除の機能をもつWEBアプリを作成することができました。
※TAGについては、現状createしかできず、TAGを削除する場合はadminサイトから行う必要があるため、今後直していければと思います

◇その他、参考にした記事

◇今後追加したい機能

  • タイトル検索、タグ検索機能の実装(現状、タグが役に立っていない・・)。
    -> こちらに追加記事を記載しました。(2019/06/02追記)

  • タグの管理メニュー(現状追加しかできない・・)。

  • 新規タグ追加画面からタグを追加した場合にtopページに飛んでしまうため、その点の修正。
    -> こちらに追加記事を記載しました。(2019/06/29追記)

  • フォームの改善(forms.pyの作成)。

  • CSSフレームワーク(Bootstrapなど)の導入(今回のアプリレベルでcssファイルが雑多になってきたため・・)

    ⇒フレームワークどーせ入れるならSass(SCSS or SASS)も試してみたい。

◇最後に

  • 今回、初めての投稿となったわけですが、記事作成に想定よりも時間がかかるというのを改めて痛感しました(結局、GW最終日の夜中まで掛かった)。。
  • 私よりわかりやすい記事を書いている方々の凄さを実感するとともに、そんな記事を載せてくれる人達のありがたみがを再認識できるGWとなりました。
  • Djangoについては、Qiita内でも多くの記事が既に出ていますので、そちらの方が参考になるかとは思いますが、少しでも参考になる方がいれば幸いです。
9
14
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
14