今回はブログアプリにMarkdown(マークダウン)を使えるように構築し、構築したMarkdownをさらに便利に扱う為のカスタマイズ(機能拡張)をしていきたいと思います。
ちなみにMarkdown(マークダウン)とは、マークダウン記法と言ってHTMLタグなどの記述を簡単な記号を使って表現できるようにされた軽量のマークアップ言語の1つです。
マークダウン記法とは別に、マークアップ言語をグラフィカルインターフェイスを使って表示などができる「Summernote(サマーノート)」というのもあります。
別記事で紹介しているので、興味のある方はこちらも参照してください。
DjangoでMarkdownを構築する方法はいくつかあって、私が知っている限りでは以下のやりかたです。
どちらとも簡単に組み込むことはできますが、今回は後者の開発されたモジュールを使って、簡単なブログアプリを例に行っていきたいと思います。
実行環境 |
---|
Windows Subsystem for Linux |
Python 3.6.8 |
pip 9.0.1 |
使用ライブラリ | ライセンス |
---|---|
Django==2.2.6 | BSD |
django-markdownx==3.0.1 | BSD |
今回使用するパッケージは「django-markdownx」です。
まだDjangoをインストールしていない方は、django-markdownxをインストールするだけで「Django」「Markdown」「Pillow」などの必要なライブラリがまとめて導入されます。
ライブラリのインストールはpipで導入します。
$ pip install django-markdownx
プロジェクトの構造は以下のようになります。
project
|-- blog
|-- migrations
|--admin.py
|--apps.py
|--models.py
|--tests.py
|--views.py
|--urls.py
|-- project
|--asgi.py
|--settings.py
|--urls.py
|--wsgi.py
|-- db.sqlite3
|--manage.py
プロジェクトとアプリを作成し、設定ファイルの編集が済みましたら、アプリディレクトリ内の「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をインストールし、モデルの構築が済んだらさっそく設定していきます。
最初にこの記事では、プロジェクトディレクトリ内にある「settings.py」から編集を始めます。
「INSTALLED_APPS」にライブラリを追記し、画像ファイルなどを集約する「media」を設定します。
※STATIC_ROOTは本番環境の際に設定してください。大変失礼致しました。
# project/project/settings.py
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'markdownx', # 追加
'blog.apps.BlogConfig', # アプリ
]
...
import os
STATIC_URL = '/static/'
# 本番環境の場合
# STATIC_ROOT = '/var/www/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('', include('blog.urls')),
path('markdownx/', include('markdownx.urls')), # 追記
]
""" メディアファイルを扱う時の開発環境時の設定 """
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
次に、アプリディレクトリ内の「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) # 追記
※モデルを変更したらマイグレーションも実行しておきましょう。
$ python3 manage.py makemigrations
$ python3 manage.py migrate
これでadminサイトの追加・編集ページでマークダウンが適用されています。
デフォルトで使用できるマークダウン一覧はこちらです。
adminサイトにてマークダウン記法共にマークアップ言語を適用させることができまたので、詳細ページまで作成しどのように表示されるか見ていきます。
では、ブログアプリを一通り完成させます。
「views.py」にて単純なビュー関数を定義します。
# project/blog/views.py
from django.shortcuts import render, get_object_or_404
from .models import Blog
""" 一覧表示 """
def index(request):
blogs = Blog.objects.order_by('-id')
context = {
'blogs': blogs
}
return render(request, 'blog/index.html', context)
""" 詳細表示 """
def detail(request, blog_id):
blog_text = get_object_or_404(Blog, id=blog_id)
context = {
'blog_text': blog_text
}
return render(request, 'blog/detail.html', context)
アプリディレクトリ内に新しく「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'),
]
アプリディレクトリ内に「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 blogs %}
<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」は、別のタイプのフォームフィールドへ渡すためのメソッドです。
「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
context = {
'form': form
}
return render(request, 'blog/form.html', context)
次にアプリディレクトリ内の「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'), # 追記
]
「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>
<br>
......
......
{% 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>
....
<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>
...
</body>
</html>
フォームのスタイルが変わったところで、画像を挿入してみます。
画像は、ドラッグアンドドロップで挿入することができます。
django-markdownxはPython-Markdownをベースに開発されているのでデフォルトで使用できるマークダウンが限られています。
django-markdownxで機能を拡張するには、Djangoプロジェクトの設定ファイル「settings.py」の下層に「MARKDOWNX_MARKDOWN_EXTENSIONS」というリストオブジェクトを置きます。
...
# マークダウンの拡張機能を格納
MARKDOWNX_MARKDOWN_EXTENSIONS = [
...,
...,
]
このリストの中に拡張したい機能を設定します。
拡張機能はPython-Markdown公式ドキュメントに記載されています。
例えば、このブログのように「目次」や「テーブル」、「コードブロック」や「改行」といったマークダウンの拡張をするのであれば以下のように設定します。
# project/project/settings.py
...
""" マークダウンのオプション設定 """
MARKDOWNX_MARKDOWN_EXTENSIONS = [
'fenced_code', # コードブロック
'tables', # テーブル
'toc', # 目次
'nl2br', # 改行
]
「MARKDOWNX_MARKDOWN_EXTENSIONS」は結果的にバックエンドで処理されているPython-Markdownライブラリの「markdown()」関数に渡されることになります。
※github.com/markdownx/utils.py markdownify()
# /markdownx/utils.py
from markdown import markdown
...
from .settings import (
MARKDOWNX_MARKDOWN_EXTENSIONS, # 拡張機能のリスト
MARKDOWNX_MARKDOWN_EXTENSION_CONFIGS # 拡張機能オプション設定の辞書
)
def markdownify(content):
"""
...
"""
# Python-Markdownライブラリ
md = markdown(
text=content,
extensions=MARKDOWNX_MARKDOWN_EXTENSIONS,
extension_configs=MARKDOWNX_MARKDOWN_EXTENSION_CONFIGS
)
return md
Python-Markdownの拡張機能やマークダウン記法の一覧に関しては別記事で実装しているので宜しければこちらをご参照ください。
コードブロックやテーブルのマークダウン記法の例は以下です。
コードブロックの記述例
# 「shift」+「@」=「`」
```python
import pandas as pd
import matplotlib.pyplot as plt
```
テーブルの記述例
# 「shift」+「¥」=「|」
|1|2|3|
|----|----|----|
|2|4|7|
画像に関する設定は、Djangoプロジェクトの設定ファイル「settings.py」の下層に「MARKDOWNX_IMAGE_MAX_SIZE」という辞書オブジェクトを置きます。
キーの「size」や「quality」に値を指定することができます。
# project/project/settings.py
...
""" 画像サイズ """
MARKDOWNX_IMAGE_MAX_SIZE = {
'size': (500, 500), 'quality': 90
}
※デフォルト値は、500×500pxとなっています。
画面サイズ(スマホやタブレット)によってレイアウトが崩れてしまう恐れがあるので、追加で少し工夫が必要になるかもしれません。
テーブルはボーダーラインを引くだけなので、「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では色々なハイライトカラーが揃っているのでぜひ試してみてください。
GIFファイルをドラッグ&ドロップできるようにするには、django-markdownxのソースコードを編集する必要があります。
以下の記事で実装しているので宜しければご参照ください。
最後にエディターのリアルタイム表記をCSSやBootStrapを使って横並びにしたいと思います。
そのためには、「markdownx」のソースコード内にある、「widget2.html」を編集する必要があります。
django-markdownxのバージョンによっては「widget.html」と「widget2.html」の2種類が存在していますが、以下のようにDjangoのテンプレートタグ「{% include 'django/forms/widgets/textarea.html' %}」が記述されているファイルがターゲットです。
<!-- markdownx/templates/markdownx/widget.html -->
<div class="markdownx">
{% include 'django/forms/widgets/textarea.html' %}
<div class="markdownx-preview"></div>
</div>
このファイルを弄る事によって、リアルタイムで表示されるテキストの位置を変更することができますが、直接弄るわけにはいきません。
なのでソースコードの「templates」ディレクトリ内の「markdownx」ディレクトリをコピーして、アプリディレクトリ内の「templates」ディレクトリ内にペーストしてしまいましょう。
まずmarkdownxのソースコードを調べるには、ターミナルにて以下のようにPythonコマンドの「-c」オプションで入力して、markdownxをインポートしてからprint関数でルートパスを調べます。
$ python3 -c "import markdownx; print(markdownx.__path__)"
['/home/user/仮想環境/lib/python3.6/site-packages/markdownx']
ディレクトリ構造は各PCによって異なると思いますが、上記のルートパスをコピーしmarkdownxディレクトリ内の構造を確認します。
# lsコマンドでディレクトリ内の構造を調べる
$ ls /home/user/仮想環境/lib/python3.6/site-packages/markdownx
__init__.py exceptions.py locale static urls.py widgets.py
__pycache__ fields.py models.py templates utils.py
admin.py forms.py settings.py tests views.py
「templates」というディレクトリを確認できたら、そのディレクトリ構造も確認します。
# lsコマンドでディレクトリ内の構造を調べる
$ ls /home/user/仮想環境/lib/python3.6/site-packages/markdownx/templates
markdownx
markdownx内のtemplates内に「markdownx」ディレクトリを確認できたら、そこまでのルートパスをコピーし、Djangoアプリ内の「templates」ディレクトリに貼り付けます。
※以下はアプリ内あるtemplatesディレクトリに貼り付けています。
# コピーコマンドの-rオプションでディレクトリをコピーし貼り付け
cp -r /home/kenno/.local/lib/python3.6/site-packages/markdownx/templates/markdownx blog/templates
アプリ内のtemplatesディレクトリ構造は以下となりました。
※admin管理画面で横並びのプレビューを行いたい場合は以下と同じようにadminサイドのテンプレートファイル群をコピーして配置させます。
# blog/templates/
templates
|-- admin # 管理画面で横並びにする場合
|--...
|--base.html
|--...
|-- blog
|-- markdownx # ペースト
|--widget.html
|--widget2.html
このアプリディレクトリ内の「templates」ディレクトリをDjangoが優先的に認識するように設定ファイルを編集します。
# project/project/settings.py
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'markdownx',
'django.forms', # 追加
'blog.apps.BlogConfig',
]
FORM_RENDERER = 'django.forms.renderers.TemplatesSetting' # 追加
...
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',
],
},
},
]
FORM_RENDERERの設定はこちら「django-markdownx テキストエリアとプレビューを横並びにする方法」を参考にさせて頂きました。
なお、Djangoのフォームレンダリングのカスタマイズに関してはこちら「Django公式ドキュメント フォームレンダリング」をご参照下さい。
「TEMPLATES」で設定した「templates」ディレクトリをプロジェクト直下のルートパスを参照するように編集しているので、templatesディレクトリを移動します。
# project/
# mvコマンドで移動。「.」指定はカレントディレクトリ
$ mv blog/templates .
目的のテンプレートファイルは以下となります。
<!-- project/templates/markdownx/widget2.html -->
<div class="markdownx">
{% include 'django/forms/widgets/textarea.html' %}
<div class="markdownx-preview"></div>
</div>
そして「widget2.html」内のクラス属性である「class="markdownx"」と「class="markdownx-preview"」にCSSを適用させ横並びにさせます。
「base.html」のstyleタグに追記していきましょう。
<!-- project/templates/blog/base.html -->
<style>
...
/* この属性以外のfloatによる意図しない回り込みは解除 */
.markdownx::after {
content: "";
display: block;
clear: both;
}
/* テキスト*/
textarea {
width: 45%;
box-sizing: border-box;
border: solid 1px;
float: left; /* 追加 */
}
/* プレビューされている属性を右側に置く */
.markdownx-preview {
width: 45%;
margin-right: 5%;
float: right;
word-break: break-all; /* テキストを幅に合わせて改行 */
}
/* 送信ボタン*/
...
</style>
以下はBootStrap適用例です。
※管理画面で適用させるには、adminのテンプレートファイルをプロジェクト直下の「templates」にコピーし、adminの「base.html」にBootstrapを設定する必要があります。
<!-- project/templates/markdownx/widget2.html -->
<div class="markdownx row">
<div class="col-md-6 col-lg-6">
{% include 'django/forms/widgets/textarea.html' %}
</div>
<div class="col-md-6 col-lg-6">
<div class="markdownx-preview"></div>
</div>
</div>
自作フォーム。
adminサイトの追加・編集ページ。
工夫次第で様々なカスタマイズを行うことができるかと思います。
それでは以上となります。
最後までご覧いただきありがとうございました。