nikkie-ftnextの日記

イベントレポートや読書メモを発信

Django Girls Tutorial ExtensionsをDjango2系で動かす(Part 1/3) #モグモグDjango

はじめに

いつも心は虹色に! nikkieです。
2018/11/10に参加した #モグモグDjango で、Django Girls Tutorial Extensions がDjango2系で動作するように確認しました。
ExtensionsをDjango 2系で動かす中で気づいたことをアウトプットします。

勉強会の概要

akiyokoさんの『現場で使えるDjangoの教科書』頒布をきっかけに始まった、Djangoもくもく会です。
第3回 モグモグDjango - connpass

Djangoもくもく会 モグモグDjango を開催します. Django初心者の方から,上級者の方までどなたでもご参加下さい(^^)

この会は,しーーっんとしているもくもく会ではなく,質問や分からないところがあったら,その場で聞くことができる環境にしたい!と思っています.

取り組んだこと

Django Gitls Tutorial(Djangoでブログアプリを作るチュートリアル)にはExtensionsという拡張版があります。
Introduction · Django Girls Tutorial: Extensions
9月のPyConJP Sprintにて有志で翻訳したのですが、ExtensionsはコードがDjango 1系ということが発覚していました。1
GitHub - DjangoGirlsJapan/tutorial-extensions: Additional tasks for tutorial (翻訳はWIP、また未レビューです)
Extensions翻訳を完了させるために、コードをDjango 2系に置き換えたいとメンバー間で考えており、今回のもくもく会で着手しました。

達成したこと

Django 2系に書き換える対象は、3ページあります。

このうち、機能追加とセキュリティについてDjango2系で動作確認できました

以下では、機能追加のパートを進める中で気づいたことを書いていきます。(セキュリティについては記事を分けます)

ブログアプリに機能追加

このパートで追加する機能は、大きく分けて3つです。

  • ドラフト(下書き)の一覧機能
  • ドラフトを公開する機能
  • 記事を削除する機能

前提

以下の環境で動かしています。

なお、機能追加のチュートリアルは、9月から11月までの間にコードがDjango2系に更新されていました。(確認しやすかったです。ありがとうございます!)

Djangoの機能追加手順

これまで手を動かしてきた経験を踏まえると、機能追加するには以下の順がよさそうです。2

  1. 新規ページへ遷移するリンクを追加
  2. 新規ページのURL設定
  3. 新規ページのビュー作成
  4. 新規ページのテンプレート作成

Djangoは機能追加する際に触る箇所が多いので、「機能追加手順のチェックリストを持つと、使いやすく感じられるかな」と考えています。

ドラフトの一覧機能

まず、ドラフト一覧画面に遷移するアイコンをヘッダーに追加します。
(記事が作成できるのはログインしているユーザのみとしていたので、ドラフトが見えるのもログインしているユーザに限定しています)

blog/templates/blog/base.html

<!-- 省略 -->
  <div class="page-header">
    {% if user.is_authenticated %}
      <a href="{% url 'post_new' %}" class="top-menu"><span class="glyphicon glyphicon-plus"></span></a>
      <a href="{% url 'post_draft_list' %}" class="top-menu"><span class="glyphicon glyphicon-edit"></span></a> <!-- 追加 -->
    {% endif %}
    <h1><a href="/">Django Girls Blog</a></h1>
  </div>
<!-- 省略 -->

次に、ドラフト一覧画面のURLを追加します。

blog/urls.py

urlpatterns = [
    # 省略
    path('drafts/', views.post_draft_list, name='post_draft_list'), # 追加
]

ビューの編集では2つのことを行います。

1つ目は、記事を作成・編集したときにpublished_dateを設定しないようにすること。
これにより、作成した記事が即時公開されなくなり、ドラフト扱いとすることができます。

  • 新規作成した場合、published_dateは空(ドラフト状態)
  • ドラフトの記事を編集した場合、published_dateは空のまま(ドラフトのまま)
  • 既に公開した記事を編集した場合、published_dateの値は変わらない(公開されたまま)

2つ目は、ドラフトの記事の一覧を表示するための関数を追加すること。
ドラフトの記事はpublished_datenullです。

blog/views.py

def post_new(request):
    if request.method == "POST":
        form = PostForm(request.POST)
        if form.is_valid():
            post = form.save(commit=False)
            post.author = request.user
            # post.published_date = timezone.now() # 削除
            post.save()
            return redirect('post_detail', pk=post.pk)
    else:
        form = PostForm()
    return render(request, 'blog/post_edit.html', {'form': form})

def post_edit(request, pk):
    post = get_object_or_404(Post, pk=pk)
    if request.method == "POST":
        form = PostForm(request.POST, instance=post)
        if form.is_valid():
            post = form.save(commit=False)
            post.author = request.user
            # post.published_date = timezone.now() # 削除
            post.save()
            return redirect('post_detail', pk=post.pk)
    else:
        form = PostForm(instance=post)
    return render(request, 'blog/post_edit.html', {'form': form})

# 以下を追加
def post_draft_list(request):
    posts = Post.objects.filter(published_date__isnull=True).order_by('created_date')
    return render(request, 'blog/post_draft_list.html', {'posts': posts})

最後に、ドラフト一覧のテンプレートを作成します。

blog/templates/blog/post_draft_list.html

{% extends 'blog/base.html' %}

{% block content %}
  {% for post in posts %}
    <div class="post">
      <p class="date">created: {{ post.created_date|date:'d-m-Y' }}</p>
      <h1><a href="{% url 'post_detail' pk=post.pk %}">{{ post.title }}</a></h1>
      <p>{{ post.text|truncatechars:200 }}</p>
    </div>
  {% endfor %}
{% endblock %}

このテンプレートには2つのフィルタが登場します。

  • date
    • 指定したフォーマットで日付を表示するフィルタ
    • d-m-Y:日(2桁)-月(2桁)-年(4桁)
  • truncatechar
    • 文字列の後ろを切り捨てて、指定した長さ(ここでは200文字)とするフィルタ
    • 切り捨てた文字列は...と3文字で表されるので、post.textの先頭から197文字が取り出される
    • ドキュメント記載の例 {{ "Joel is a slug"|truncatechars:9 }}Joel i...Joel iの6文字+...の3文字で合計9文字)

ここまでで、ドラフトボタンからドラフト一覧画面に遷移できます。
f:id:nikkie-ftnext:20181110195137p:plain

ドラフトを公開する機能

まず、記事の詳細画面にドラフトを公開するボタンを作ります。
(公開されていない記事=ドラフトの場合のみ、公開ボタンが見えるようにしています3

blog/templates/blog/post_detail.html

    {% if post.published_date %}
      <div class="date">
        {{ post.published_date }}
      </div>
    {% else %} <!-- 追加 -->
      <a class="btn btn-default" href="{% url 'post_publish' pk=post.pk %}">Publish</a> <!-- 追加 -->
    {% endif %}

次に、ドラフト公開に使うURLを追加します。
Extensionsの原文のまま('post/<pk>/publish/')でもビューのget_object_or_404は動きましたが、チュートリアル本編のURLの設定に合わせて'post/<int:pk>/publish/'としています。4

blog/urls.py

urlpatterns = [
    # 省略
    path('drafts/', views.post_draft_list, name='post_draft_list'),
    path('post/<int:pk>/publish/', views.post_publish, name='post_publish'),  # 追加
]

最後にビューにpost_publish関数を追加します。

blog/views.py

def post_publish(request, pk):
    post = get_object_or_404(Post, pk=pk)
    post.publish()
    return redirect('post_detail', pk=pk)

追加した関数で使われているpublishメソッドですが、これは Djangoモデル · Django Girls Tutorial で定義したものです。
Extensionsにて、伏線がようやく回収されました!

記事を公開した後は記事詳細にリダイレクトするので、新規で作成するテンプレートはありません。

以上で、ドラフト公開ボタンが付きました!
f:id:nikkie-ftnext:20181110195249p:plain

記事を削除する機能

記事の削除機能の追加は、ドラフト公開機能と同様の流れです。

記事の削除ボタンが見えるのは、ログインしているユーザのみです。

blog/templates/blog/post_detail.html

    {% if user.is_authenticated %}
      <a class="btn btn-default" href="{% url 'post_edit' pk=post.pk %}"><span class="glyphicon glyphicon-pencil"></span></a>
      <a class="btn btn-default" href="{% url 'post_remove' pk=post.pk %}"><span class="glyphicon glyphicon-remove"></span></a> <!-- 追加 -->
    {% endif %}

削除機能の場合も、URLの設定はチュートリアル本編の記載に合わせています。

blog/urls.py

urlpatterns = [
    # 省略
    path('drafts/', views.post_draft_list, name='post_draft_list'),
    path('post/<int:pk>/publish/', views.post_publish, name='post_publish'),
    path('post/<int:pk>/remove/', views.post_remove, name='post_remove'),  # 追加
]

blog/views.py

def post_remove(request, pk):
    post = get_object_or_404(Post, pk=pk)
    post.delete()
    return redirect('post_list')

deleteDjangoモデルが持っているメソッドだそうです。

こうして、記事が削除できるようになりました!
f:id:nikkie-ftnext:20181110195316p:plain

感想

Django Girls Tutorialの懇切丁寧な解説から方向転換し、Extensionsは「気になるところは自分で調べてね」という感じです。(独り立ちを意識しているのかもしれません)
ファイルの変更箇所を明確にする意図でこの記事は詳しく書いていますが、実際に手を入れる箇所はそんなに多くはありませんでした。
下書き機能もついて、ブログアプリの機能が充実してきて嬉しい限りです^ ^

教えていただいたもの

今回アウトプットが遅くなってしまいましたが、モグモグDjango参加者、運営者の皆さま、どうもありがとうございました!
Extensionsはperfectじゃなくてもいいので、なる早でdoneに持っていく所存です。

脚注


  1. path関数ではなくurl関数が使われていたため、発覚しました。

  2. 参照 週末ログ | Django Girls Tutorial翻訳レビュー 100%到達! - nikkie-ftnextの日記 なお、機能追加にモデルが必要な場合は作成が必要です。(ビューの前に作ることになると思います)

  3. ブログを書いていて気づいたのですが、Extensionsのとおりに進めると、ログインしていないユーザでも公開できてしまうみたいですね。セキュリティのパートを終えると、ログインしているユーザだけが公開できるようになりました

  4. 原文のまま('post/<pk>/publish/')だとstrのpk(例:'1')でget_object_or_404が動きます。'post/<int:pk>/publish/'とすると、intのpk(例:1)がget_object_or_404に渡ります。