ブログの記事詳細ページを作る

Python Django

概要

Djangoで、ブログを作るシリーズ①の1つです。ブログの記事詳細ページを作成していきます。

ビューの作成

urls.pyに追記します。

from django.urls import path
from . import views

app_name = 'nblog1'

urlpatterns = [
    path('', views.PublicPostIndexView.as_view(), name='top'),
    path('private/', views.PrivatePostIndexView.as_view(), name='private_post_list'),
    path('detail/<int:pk>/', views.PostDetailView.as_view(), name='post_detail'),  # 追加
]

views.pyに追記します。

# 追加
from django.http import Http404


# 追加
class PostDetailView(generic.DetailView):
    """記事詳細ページを表示する。"""
    model = Post

    def get_queryset(self):
        return super().get_queryset().prefetch_related('tags', 'comment_set__reply_set')

    def get_object(self, queryset=None):
        """その記事が公開か、ユーザがログインしていれば表示する。"""
        post = super().get_object()
        if post.is_public or self.request.user.is_authenticated:
            return post
        else:
            raise Http404

get_querysetは、パフォーマンス上の問題のために上書きしています。

get_objectですが、これはログインしているか、記事が公開設定ならOKという指定です。そうでない場合は、404ページを表示します。

テンプレートフィルタの作成

今回ですが、記事本文はマークダウンで書く予定です。マークダウンテキストをHTMLに変換する必要があります。

まず、pip install markdownとして必要なライブラリをインストールしておきましょう。そしてtemplatetags/nblog1.pyを開き、テンプレートフィルタを作成します。

from django import template
from django.conf import settings
from django.utils.safestring import mark_safe
import markdown
from markdown.extensions import Extension

register = template.Library()


# これは元からある
@register.simple_tag
def url_replace(request, field, value):
    """GETパラメータの一部を置き換える。"""
    url_dict = request.GET.copy()
    url_dict[field] = str(value)
    return url_dict.urlencode()


@register.filter
def markdown_to_html(text):
    """マークダウンをhtmlに変換する。"""
    html = markdown.markdown(text, extensions=settings.MARKDOWN_EXTENSIONS)
    return mark_safe(html)


class EscapeHtml(Extension):

    def extendMarkdown(self, md):
        md.preprocessors.deregister('html_block')
        md.inlinePatterns.deregister('html')


@register.filter
def markdown_to_html_with_escape(text):
    """マークダウンをhtmlに変換する。
    生のHTMLやCSS、JavaScript等のコードをエスケープした上で、マークダウンをHTMLに変換します。
    公開しているコメント欄等には、こちらを使ってください。
    """
    extensions = settings.MARKDOWN_EXTENSIONS + [EscapeHtml()]
    html = markdown.markdown(text, extensions=extensions)
    return mark_safe(html)

マークダウンをHTMLに変換するmarkdown_to_htmlmarkdown_to_html_with_escapeの2つのフィルタを作っています。

今回はコメント等もマークダウンで書けるようにするのですが、マークダウンにはHTMLやCSS、JavaScriptのコードを直接書くこともできます。なので、悪意のある第三者が無限アラートを出すようなJSコードをコメントとして書くかもしれません。それを防ぐために、markdown_to_html_with_escapeという生のHTML・CSS・JavaScriptコードをエスケープ処理するフィルタを作っています。

マークダウンには様々な拡張があり、それらの導入をsettings.pyに定義できるようにしました。とりあえず、settings.pyに次のように追記しておきましょう。

# マークダウンの拡張
MARKDOWN_EXTENSIONS = [
    'markdown.extensions.extra',
    'markdown.extensions.toc',
]

テンプレートファイルの作成

まず先に、post_list.htmlを開いてリンクを修正しましょう。

<h2 class="post-title"><a href="{% url 'nblog1:post_detail' post.pk %}">{{ post.title }}</a></h2>

post_detail.htmlを作り、中身を次のようにします。

{% extends 'nblog1/base.html' %}
{% load nblog1 %}
{% load static %}
{% load humanize %}
{% block meta_title %}{{ post.title }} - {{ block.super }}{% endblock %}
{% block meta_description %}{{ post.description }}{% endblock %}
{% block meta_keywords %}{{ post.keywords }}{% endblock %}

{% block content %}
    <nav id="back"><a href="{% url 'nblog1:top' %}">TOPへ戻る</a></nav>
    <article class="post" id="post-detail">
        <h1 class="post-title">
            {% if user.is_authenticated %}
                <a href="{% url 'admin:nblog1_post_change' post.pk %}" target="_blank">{{ post.title }}</a>
            {% else %}
                {{ post.title }}
            {% endif %}
        </h1>

        <div>
            <time class="updated_at" datetime="{{ post.updated_at | date:'Y-m-d' }}">{{ post.updated_at | naturaltime }}に更新
            </time>

            {% for tag in post.tags.all %}
                <span class="tag-no-click">{{ tag.name }}</span>
            {% endfor %}
        </div>

        <div class="markdown-body">
            {{ post.text | markdown_to_html }}
        </div>
    </article>

{% endblock %}

{% block extrahead %}
    {{ block.super }}
    <!-- コードシンタックス -->
    <link rel="stylesheet" href="//cdn.jsdelivr.net/gh/highlightjs/cdn-release@9.16.2/build/styles/dracula.min.css">
    <script src="//cdn.jsdelivr.net/gh/highlightjs/cdn-release@9.16.2/build/highlight.min.js"></script>
    <script>hljs.initHighlightingOnLoad();</script>
{% endblock %}

そして、style.cssに追記しましょう。

strong {
    background-color: yellow;
}

img {
    max-width: 100%;
    height: auto;
    border: solid 1px #ccc;
}

li {
    margin: 0.5em 0;
}

li > ul {
    margin-top: 5px;
}

pre {
    line-height: 1.3;
}

blockquote {
    padding: 15px;
    border-left: 5px solid #ccc;
    border-radius: 2px;
}

h1.post-title {
    line-height: 1.5;
    font-size: 24px;
    font-weight: bold;
}

.tag-no-click {
    margin-right: 5px;
    color: #6F959E;
    font-size: 14px;
}


/*
記事詳細ページ
 */
#back {
    margin-bottom: 48px;
}

/* マークダウン関連 */
.markdown-body > * {
    margin-top: 1em;
    margin-bottom: 1em;
}

.markdown-body > h2, .markdown-body > h3 {
    font-size: 24px;
    margin-top: 100px;
}

.toc > ul {
    list-style-type: none;
    background-color: #f5f5f5;
    padding: 10px;
}

特に書き間違いがなければ、次のようなカッコいいページが表示されます。

関連記事欄の作成

Postモデルは、relation_posts = models.ManyToManyField('self', verbose_name='関連記事', blank=True)というフィールドがありました。これは関連記事を指定するためのフィールドです。

この記事に紐づいた関連記事の一覧を表示するセクションを作ります。

post_defail.htmlに追記します。

    <section id="relation-posts">
        <h2 class="section-title">Relation Posts</h2>
        {% for post in post.relation_posts.all %}
            <article class="post">
                <h3 class="post-title"><a href="{% url 'nblog1:post_detail' post.pk %}">{{ post.title }}</a></h3>
                <p class="description">{{ post.description }}</p>

                <div>
                    <time class="updated_at"
                          datetime="{{ post.updated_at | date:'Y-m-d' }}">{{ post.updated_at | naturaltime }}に更新
                    </time>

                    {% for tag in post.tags.all %}
                        <span class="tag-no-click" data-pk="{{ tag.pk }}">{{ tag.name }}</span>
                    {% endfor %}
                </div>
            </article>
        {% empty %}
            <p>関連記事はありません。</p>
        {% endfor %}
    </section>

{% endblock %}

{% for post in post.relation_posts.all %}のように関連記事を取り出して、記事一覧ページと同様に一覧で表示しています。

CSSも追記しておきます。

.section-title {
    font-size: 24px;
    font-weight: bold;
}

#relation-posts h2 {
    margin-top: 100px;
    margin-bottom: 10px;
    border-bottom: solid 1px #ccc;
    padding-bottom: 10px;
}

コメント欄

このブログでは記事へのコメントや、コメントへの返信ができます。それらを表示するように設定していきます。まず、post_detail.htmlに追記です。

    <section id="comment">
        <h2 class="section-title">Comment</h2>
        <p><a href="" target="_blank" rel="nofollow">記事にコメントする</a></p>
        <!-- コメント一覧 -->
        {% for comment in post.comment_set.all %}
            <div class="comment">
                <h3>{{ comment.name }}</h3>
                <time class="updated_at"
                      datetime="{{ comment.created_at | date:'Y-m-d' }}">{{ comment.created_at | naturaltime }}</time>
                <div class="description markdown-body">
                    {{ comment.text | markdown_to_html_with_escape}}
                </div>
                <p>
                    <a href="" target="_blank"
                       rel="nofollow">返信する</a>
                </p>
            </div>


            <!-- リプライ一覧 -->
            {% for reply in comment.reply_set.all %}
                <div class="reply">
                    <h3>{{ reply.name }}</h3>
                    <time class="updated_at"
                          datetime="{{ reply.created_at | date:'Y-m-d' }}">{{ reply.created_at | naturaltime }}</time>
                    <div class="description markdown-body">
                        {{ reply.text | markdown_to_html_with_escape }}
                    </div>
                </div>
            {% endfor %}
            <!-- リプライ一覧終わり -->

        {% empty %}
            <p>まだコメントはありません。</p>
        {% endfor %}
        <!-- コメント一覧終わり -->
    </section>

{% endblock %}

CSSも追記します。


#comment h2, #relation-posts h2 {
    margin-top: 100px;
    margin-bottom: 10px;
    border-bottom: solid 1px #ccc;
    padding-bottom: 10px;
}

/* コメント欄 */
.comment, .reply {
    margin-top: 48px;
}

.comment > h3, .reply > h3 {
    font-size: 18px;
}

@media (min-width: 1024px) {
    #comment {
        display: grid;
        grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);

    }

    #comment > * {
        grid-column: 1 / -1;
    }

    #comment > .comment {
        grid-column: 1 / -1;
    }

    #comment > .reply {
        grid-column: 2;
    }
}

これでコメントが表示されるようになりました。次回はコメントや返信の投稿ページを作っていきます。

Relation Posts

django-markdownxの紹介

django-markdownを使い、DjangoアプリケーションにMarkdownエディタを導入していくサンプルです。

Python Django Djangoライブラリ Markdown

Comment

記事にコメントする

まだコメントはありません。