【Django】ブログアプリにマークダウン(django-markdownx)を構築しカスタマイズする

投稿日2019年11月19日 / 更新日2020年3月1日



今回はブログアプリにMarkdown(マークダウン)を使えるように構築し、構築したMarkdownをさらに便利に扱う為のカスタマイズ(機能拡張)をしていきたいと思います。


ちなみにMarkdown(マークダウン)とは、マークダウン記法と言ってHTMLタグなどの記述を簡単な記号を使って表現できるようにされた軽量のマークアップ言語の1つです。


マークダウン記法とは別に、マークアップ言語をグラフィカルインターフェイスを使って表示などができる「Summernote(サマーノート)」というのもあります。


別記事で紹介しているので、興味のある方はこちらも参照してください。




DjangoでMarkdownを構築する方法はいくつかあって、私が知っている限りでは以下のやりかたです。



  • Pythonの外部ライブりである「markdown」をインストールしてアプリに組込み、ツールバーやリアルタイム(プレビュー)表示をさせたい場合は追加でWYSIWYG(ウィジウィグ)エディターのフレームワークを別途CSS/Java Scriptのモジュールを読み込ませる。

  • APIのように簡単に構築できるよう有志の方々が開発されたされたモジュールを使う。


どちらとも簡単に組み込むことはできますが、今回は後者の開発されたモジュールを使って、簡単なブログアプリを例に行っていきたいと思います。


それでは始めていきましょう。


Python 3.6.8
Django==2.2.6
django-markdownx==2.0.28


【目次】



アプリのモデルを作成


プロジェクトとアプリを作成し、設定ファイルの編集が済みましたら、アプリディレクトリ内の「models.py」を編集します。


project/blog/models.py


from django.db import models


class Blog(models.Model):
title = models.CharField('タイトル', max_length=50)
text = models.TextField('テキスト')
created_at = models.DateField('作成日', auto_now_add=True)
updated_at = models.DateField('更新日', auto_now=True)
def __str__(self):
return self.title
class Meta:
verbose_name = 'ブログ'
verbose_name_plural = 'ブログ'




「admin.py」を編集します。


project/blog/admin.py


from django.contrib import admin
from .models import Blog


admin.site.register(Blog)




ターミナルにてデータベースに反映させるためにマイグレーションを行い、スーパーユーザーを作成します。



$ python3 manage.py makemigrations

$ python3 manage.py migrate

$ python3 manage.py createsuperuser

$ python3 manage.py runserver
127.0.0.1:8000



「127.0.0.1:8000/admin」へアクセスすると以下のようになります。



django-markdownxを構築


ではさっそくマークダウンを構築していきます。


今回使用するパッケージは「django-markdownx」です。


こちらのパッケージは資料も充実しており、画像の挿入やリアルタイムで表示されるテキストエディターを使うことができるので大変便利です。


公式ドキュメントと合わせて進めると助けになると思うのでよければそちらもご参照ください。


まずはパッケージのインストールをします。



$ pip3 install django-markdownx



インストールが終わりましたら、プロジェクトディレクトリ内の「settings.py」を編集します。


project/project/settings.py


INSTALLED_APPS = [
'blog.apps.BlogConfig', # アプリ
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'markdownx', # 追加
]


STATIC_URL = '/static/'
""" 追記 """
STATIC_ROOT = os.path.join(BASE_DIR, "static/")


""" 追記 """
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media/')




次にプロジェクトディレクトリ内の「urls.py」を編集します。


project/project/urls.py


from django.contrib import admin
from django.urls import path, include # 追記


""" 追記 """
from django.conf import settings
from django.conf.urls.static import static


urlpatterns = [
path('admin/', admin.site.urls),
path('markdownx/', include('markdownx.urls')), # 追記
]


""" メディアファイルを扱う時の開発環境時の設定 """
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)




編集が終わりましたら、ターミナルにて「collectstatic」を行います。


これを行うことによって、プロジェクト直下にdjango-markdownxのファイルが集められている「static」ディレクトリが作成されます。



$ python3 manage.py collectstatic



project/


project
|--blog
|--project
|--static ←作成される
|--db.sqlite3
|--manage.py




次に、アプリディレクトリ内の「models.py」を編集します。


project/blog/models.py


from django.db import models


""" 追記 """
from markdownx.models import MarkdownxField


class Blog(models.Model):
title = models.CharField('タイトル', max_length=50)
text = MarkdownxField('テキスト') # 変更
created_at = models.DateField('作成日', auto_now_add=True)
updated_at = models.DateField('更新日', auto_now=True)
def __str__(self):
return self.title
class Meta:
verbose_name = 'ブログ'
verbose_name_plural = 'ブログ'




textフィールドの「models.TextField」を、django-markdownxの「MarkdownxField」に変更します。


※この後に詳細表示にてマークダウンを適用させるカスタムメソッドの定義もしていきます。


モデルを変更したらマイグレーションも実行しておきましょう。


ひとまずadminサイトでの挙動を確認したいので、「admin.py」を編集してアクセスしてみましょう。


project/blog/admin.py


from django.contrib import admin
from .models import Blog


""" 追記 """
from markdownx.admin import MarkdownxModelAdmin


admin.site.register(Blog, MarkdownxModelAdmin) # 追記




これでadminサイトの追加・編集ページでマークダウンが適用されています。



テンプレートを作成し詳細表示させる


adminサイトにてマークダウン記法共にマークアップ言語を適用させることができまたので、詳細ページまで作成しどのように表示されるか見ていきます。


では、ブログアプリを一通り完成させます。


プロジェクトディレクトリ内の「urls.py」を編集します。


project/project/urls.py


from django.contrib import admin
from django.urls import path, include


from django.conf import settings
from django.conf.urls.static import static


urlpatterns = [
.......
.......
path('', include('blog.urls')), # 追加
]


......




アプリディレクトリ内に新しく「urls.py」を作成します。


project/blog/urls.py


from django.urls import path
from . import views


app_name = 'blog'


urlpatterns = [
path('', views.index, name='index'),
path('detail/<int:blog_id>/', views.detail, name='detail'),
]




「views.py」にて単純なビュー関数を定義します。


project/blog/views.py


from django.shortcuts import render, get_object_or_404
from .models import Blog


""" 一覧表示 """
def index(request):
blog = Blog.objects.order_by('-id')
return render(request, 'blog/index.html',
{'blog': blog })


""" 詳細表示 """
def detail(request, blog_id):
blog_text = get_object_or_404(Blog, id=blog_id)
return render(request, 'blog/detail.html', {'blog_text': blog_text })




アプリディレクトリ内に「templates/blog」ディレクトリを作成し、「base.html」「index.html」「detail.html」をそれぞれ作成します。


project/blog/


blog
|--__pycache__
|--migrations
|--templates
| |--blog ←作成
|
|--admin.py
|--........




テンプレートのベースとなる「base.html」。


project/blog/templates/blog/base.html


<!doctype html>
<html>
<head>
<title>Blog</title>
</head>
<body>
<h1>Blog</h1>
{% block content %}
{% endblock %}
</body>
</html>




一覧表示用の「index.html」。


project/blog/templates/blog/index.html


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


{% block content %}


<h1>一覧</h1><br>
{% for blog in blog %}
<ul>
<li>
{{ blog.created_at }}
{{ blog.title }}
<a href="{% url 'blog:detail' blog_id=blog.id %}">詳細</a>
</li>
</ul>
{% endfor %}


{% endblock %}




詳細表示用の「detail.html」。


project/blog/templates/blog/detail.html


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


{% block content %}


<div class="detail_field">
<h1>{{ blog_text.title }}</h1>
<p>{{ blog_text.updated_at }}</p>
<div class="markdownx">
{{ blog_text.text | safe }}<hr /></br>
</div>
<a href="{% url 'blog:index' %}">トップページに戻る</a>
</div>


{% endblock %}




詳細表示される部分は、後ほどスタイルを変えていくので「div class="markdownx"」として囲っています。


フィルタタグに「safe」と入れることで、HTMLのエスケープを解除できます。


Djangoのフィルタタグ「{{ linebreaks }}」(改行)は、Markdownと競合してしまいレイアウトが崩れる恐れがあるのでMarkdownのオプション設定で拡張させます。


それではシステムを再起動し、表示してみましょう。



HTMLタグで囲った「見出し」はエスケープを解除したことにより上手く適用されていますが、マークダウン記法の「見出し」部分は適用されていません。


マークダウンを適用させるにはいくつか方法がありますが、ここではブログモデルのクラスにカスタムメソッドを定義していきます。


それでは「models.py」を編集していきましょう。


project/blog/models.py


from django.db import models


from markdownx.models import MarkdownxField
""" 追記 """
from django.utils.safestring import mark_safe
from markdownx.utils import markdownify


class Blog(models.Model):
title = models.CharField('タイトル', max_length=50)
text = MarkdownxField('テキスト')
created_at = models.DateField('作成日', auto_now_add=True)
updated_at = models.DateField('更新日', auto_now=True)
""" カスタムメソッド """
def get_text_markdownx(self):
return mark_safe(markdownify(self.text))
def __str__(self):
return self.title
class Meta:
verbose_name = 'ブログ'
verbose_name_plural = 'ブログ'




任意の関数名「get_text_markdownx」は、テキストフィールドを「markdownify」メソッドによりマークダウンタグを解析し文字列を出力するようにしています。


「mark_safe」メソッドは、出力される文字列が安全であるかマークするために使用していますが、特にエンドユーザーが入力するような「コメント欄」などで使用します。


詳しくはDjnago公式ドキュメントへ。


「markdownify」についても、django-markdownxの公式ドキュメントに載っていますので詳しくはそちらもご参照ください。


そして詳細ページでは、先ほど定義したカスタムメソッドを表示させるように変更します。


project/blog/templates/blog/detail.html


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


{% block content %}


<div class="detail_field">
<h1>{{ blog_text.title }}</h1>
<p>{{ blog_text.updated_at }}</p>
<div class="markdownx">
<!-- 変更 -->
{{ blog_text.get_text_markdownx | safe }}<hr /></br>
</div>
<a href="{% url 'blog:index' %}">トップページに戻る</a>
</div>


{% endblock %}




するとマークダウン記法が適用されるようになります。



この方法以外にも、フィルタタグを自作した方法を紹介しているサイトがあります。


非常に参考にさせて頂きました。



フォームの作成


次に自作フォームを作成していきましょう。


アプリディレクトリ内に新しく「forms.py」を作成します。


project/blog/forms.py


from django import forms
from .models import Blog


from markdownx.widgets import MarkdownxWidget


class BlogForm(forms.ModelForm):
class Meta:
model = Blog
fields = ('title', 'text')
widgets = {
'text': MarkdownxWidget(),
}




「MarkdownxWidget」は、別のタイプのフォームフィールドへ渡すためのメソッドです。


次にアプリディレクトリ内の「urls.py」を編集します。


project/blog/urls.py


from django.urls import path
from . import views


app_name = 'blog'


urlpatterns = [
........
........
path('add_form', views.add_form, name='add_form'), # 追記
]




「views.py」を編集します。


project/blog/views.py


from django.shortcuts import render, get_object_or_404, redirect # 追記
from .models import Blog


""" 追記 """
from .forms import BlogForm


.........


.........


""" 自作フォーム """
def add_form(request):
if request.method == "POST":
form = BlogForm(request.POST)
if form.is_valid():
form.save()
return redirect('blog:index')
else:
form = BlogForm
return render(request, 'blog/form.html', {'form': form })




「templates/blog」ディレクトリ内に「form.html」を新しく作成します。


project/blog/templates/blog/form.html


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


{% block content %}


<a href="{% url 'blog:index' %}">トップページに戻る</a>


<form action="{% url 'blog:add_form' %}" method="POST">
{% csrf_token %}
{{ form.as_p }}
{{ form.media }}
<button type="submit">投稿</button>
</form>


{% endblock %}




最後に「index.html」に追加リンクを追記します。


project/blog/templates/blog/index.html


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


{% block content %}


<h1>一覧</h1><br>




<a href="{% url 'blog:add_form' %}">追加</a>


......
......


{% endblock %}




システムを再起動させ、追加フォームを表示させてみましょう。



フォームが少し格好悪いので、スタイルを変更するために「form.html」を編集します。


project/blog/templates/blog/form.html


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


{% block content %}


<a href="{% url 'blog:index' %}">トップページに戻る</a>


<form action="{% url 'blog:add_form' %}" method="POST">
{% csrf_token %}
{% for field in form %}
<div class="field">
{{ field }}
{{ form.media }}
</div>
{% endfor %}
<button type="submit">投稿</button>
</form>


{% endblock %}




そして「base.html」のheadタグ内にstyleタグを追記しCSSを適用させます。


project/blog/templates/blog/base.html


<!doctype html>
<html>
<head>
<title>Blog</title>
<style>
.field {
margin-top: 20px;
}
/* ブロック要素にする*/
label {
display: block;
}
/* タイトル */
input {
width: 45%;
padding: 5px;
box-sizing: border-box;
border: solid 1px;
}
/* テキスト*/
textarea {
width: 45%;
box-sizing: border-box;
border: solid 1px;
}
/* 送信ボタン*/
button {
margin-top: 30px;
width: 10%;
padding: 5px;
}
</style>
</head>
<body>
<h1>Blog</h1>
{% block content %}
{% endblock %}
</body>
</html>




フォームのスタイルが変わったところで、画像を挿入してみます。


画像は、ドラッグアンドドロップで挿入することができます。



カスタマイズ(機能拡張)


デフォルトの状態では、「[TOC]」による目次の表示やコードのハイライト、テーブルの表示・改行といった事が行えません。


django-markdownxではDjangoの設定ファイルにオプションを追加することで機能拡張を図れます。


さっそく上記の設定を行ってみましょう。


プロジェクトディレクトリ内の「settings.py」を開きます。


project/project/settings.py


........
........


MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media/')


""" マークダウンのオプション設定 """
MARKDOWNX_MARKDOWN_EXTENSIONS = [
'markdown.extensions.extra', # コードのハイライト
'markdown.extensions.toc', # 目次
'markdown.extensions.tables', # テーブル
'markdown.extensions.nl2br', # 改行
]




これらのオプションは、Python-Markdownの公式ドキュメントで調べることができます。



挿入される画像のサイズに関しては、以下のように設定します。


project/project/settings.py


........
........


MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media/')


""" 画像サイズ """
MARKDOWNX_IMAGE_MAX_SIZE = {
'size': (500, 500), 'quality': 90
}




※デフォルト値は、500×500pxとなっています。画面サイズ(スマホやタブレット)によってレイアウトが崩れてしまう恐れがあるので、追加で少し工夫が必要になるかと思います。


では「目次」「コード」「テーブル」をそれぞれマークダウン記法で表示してみましょう。


コードの記法例



shift+@=`
```python
import pandas as pd
import matplotlib.pyplot as plt
```



テーブルの記法例



shift+¥=|
|1|2|3|
||||
|2|4|7|




「目次」はいいとして、「コード」「テーブル」が少し見難いので強調表示させます。


テーブルのスタイルを変更


テーブルはボーダーラインを引くだけなので、「base.html」のstyleタグ内にCSSを記述します。


ブログの詳細表示では「div class="markdownx"」で囲っているので、このおかげで的を絞って適用できます。


project/blog/templates/blog/base.html


<style>
.......
.......


/* テーブル*/
.markdownx table {
border-collapse: collapse;
}
/* テーブル*/
.markdownx th,td {
border: solid 2px;
}
/* このブロックの枠を制限*/
.detail_field {
margin-right: 70%;
}
</style>





シンタックスハイライト


次にコードの部分ですが、シンタックスハイライトの「highlight.js」を使って強調表示させてみたいと思います。


導入方法に関してはこの記事では割愛させていただきますので、詳しくは「highlight.js公式ドキュメント」か、過去記事の「【Django】django-summernoteを使ってブログアプリに便利なテキストエディターを構築」で説明しているので、そちらの方をご参照ください。


highlight.jsからコピーしたCDNのコードを「base.html」にそれぞれ貼り付けます。


project/blog/templates/blog/base.html


<!doctype html>
<html>
<head>
<title>Blog</title>
<!-- highlight.js-->
<link rel="stylesheet"href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@9.16.2/build/styles/default.min.css">
........
..........
</head>
<body>
.......
.......
<!-- highlight.js-->
<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@9.16.2/build/highlight.min.js"></script>
<!-- highlight.js-->
<script>hljs.initHighlightingOnLoad();</script>
</body>
</html>




highlight.jsでは色々なハイライトカラーが揃っているのでぜひ試してみてください。



エディターのリアルタイム表記を横並びにカスタム


最後にエディターのリアルタイム表記を簡単なCSSを使って横並びにしたいと思います。


そのためには、「markdownx」のソースコード内にある、「widget2.html」を編集する必要があります。


widget2.htmlの中身は以下のようになっています。


markdownx/templates/markdownx/widget2.html


<div class="markdownx">
{% include 'django/forms/widgets/textarea.html' %}
<div class="markdownx-preview"></div>
</div>




このファイルを弄る事によって、リアルタイムで表示されるテキストの位置を変更することができますが、直接弄るわけにはいきません。


なのでソースコードの「templates」ディレクトリ内の「markdownx」ディレクトリをコピーして、アプリディレクトリ内の「templates」ディレクトリ内にペーストしてしまいましょう。


まずmarkdownxのソースコードを調べるには、ターミナルにて以下のようにコマンド入力します。



$ python3 -c "import markdownx; print(markdownx.__path__)"
['/home/user/仮想環境/lib/python3.6/site-packages/markdownx']



上記パスを辿り、「markdownx/templates」ディレクトリ内の「markdownx」をコピーしてアプリディレクトリ内の「templates」ディレクトリ内へペーストします。


project/blog/templates/


templates
|--blog
|--markdownx # ペースト
|--.DS_Store
|--widget.html
|--widget2.html




「widget.html」の方はDjango1.10以下のバージョンが適用されます。


このアプリディレクトリ内の「templates」ディレクトリをDjangoが優先的に認識するように設定ファイルを編集します。


project/project/settings.py


TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR, 'templates')], # 追記
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]




そして「widget2.html」を編集します。


※以下はそれらしく記述したものです。


project/blog/templates/markdownx/widget2.html


<div class="markdownx">
<div>
{% include 'django/forms/widgets/textarea.html' %}
</dvi>
<div style="float: right; margin-left: 5px;">
<div class="markdownx-preview"></div>
</div>
</div>




BootStrap適用例(公式ドキュメントより)。


project/blog/templates/markdownx/widget2.html


<div class="markdownx row">
<div class="col-md-6">
{% include 'django/forms/widgets/textarea.html' %}
</div>
<div class="col-md-6">
<div class="markdownx-preview"></div>
</div>
</div>




自作フォーム。



adminサイトの追加・編集ページ。



工夫次第で様々なカスタマイズを行うことができるかと思います。


それでは以上となります。


Twitterの方で新着記事投稿のお知らせなどをツイートしていますので、最速で受け取りたい方はぜひフォローをして頂ければありがたいです。


最後までご覧いただきありがとうございました。


トップページに戻る

Follow @kenno_w