DjangoでMarkdown記法を使う環境をセットアップする手順

こんにちは。
株式会社クラスアクト インフラストラクチャ事業部の大塚です。

今回はタイトルにもある通り、DjangoでMarkdown記法を使う環境をセットアップしていきたいと思います。
ざっと調べてみたところAPIとかで実装する方法もあるっぽいのですが、今回はdjango-markdownxを使って環境を作っていきます。

# 用語
## Markdown記法

Wikipedia様から拝借します。
Qiitaを使っている皆様であればおなじみの記法ですね。
Djangoはデフォルトでこの記法が使えないっぽいので、例えばDjangoでBlogサイトを運営していこうと考えた時、HTML/CSSでべた書きしていかないといけないっぽく、不便っぽいです。

>文書を記述するための軽量マークアップ言語のひとつである。本来はプレーンテキスト形式で手軽に書いた文書からHTMLを生成するために開発されたものである。しかし、現在ではHTMLのほかパワーポイント形式やLATEX形式のファイルへ変換するソフトウェア（コンバータ）も開発されている。

https://ja.wikipedia.org/wiki/Markdown#:~:text=Markdown%EF%BC%88%E3%83%9E%E3%83%BC%E3%82%AF%E3%83%80%E3%82%A6%E3%83%B3%EF%BC%89%E3%81%AF%E3%80%81,%E3%82%82%E9%96%8B%E7%99%BA%E3%81%95%E3%82%8C%E3%81%A6%E3%81%84%E3%82%8B%E3%80%82

## django-markdownx

Django用のプラグインみたいです。

>Django MarkdownX は、有名な高レベル Python Web フレームワークであるDjango用に構築された包括的なMarkdownプラグインであり、柔軟性、拡張性、使いやすさを核にしています。

https://neutronx.github.io/django-markdownx/

# 参考サイト

https://blog.narito.ninja/detail/102#google_vignette

https://zerofromlight.com/blogs/detail/52/#google_vignette

# 環境イメージ
今回は以下の様な環境で構築しております。
※ほかにもファイルはありますが、編集しているファイルだけ表記してます。

![django-ページ20.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3219385/d5be3b0a-6ae6-eb6b-bbcb-c5a7d03aeec5.png)

# 環境構築
## 設定入れ込み
Djangoのプロジェクトとアプリケーションを作成します。
その後、以下のコマンドを実行しdjango-markdownxをインストールします。
```
PS C:\Users\ohtsu\Documents\DjangoEnv\django-dev\blogPJ> pip install django-markdownx
```
私の開発環境はanacondaなので、anaconda navigatorでインストールされていることを確認します。
![2024021801.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3219385/e27dd2f9-1fe8-48ef-af92-9bdfe5286181.png)

プロジェクトのsettings.pyを編集していきます。
編集したものだけ記載します。
```blogPJ/settings.py
import os

~中略~

ALLOWED_HOSTS = ["*"]

~中略~

INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "markdownx",
	"blogApp",
]

~中略~

LANGUAGE_CODE = "ja"
TIME_ZONE = "Asia/Tokyo"
USE_I18N = True
USE_TZ = True

~中略~

# Blogに掲載する画像等の格納場所を設定
MEDIA_URL = "/media/"
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
```

続いてurls.pyとなります。
path('markdownx/', include('markdownx.urls'))に関しては、今はおまじないとでも思っておけばいいと思います。このように書くといいらしいです。
※mediaディレクトリの配下にmarkdownxディレクトリが自動で生成されるので「このディレクトリ作っていないんだけど、作らないといけないの？」とかは思わなくていいです。
```blogAP/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('blogApp.urls')),  
    path('markdownx/', include('markdownx.urls')), 
]

urlpatterns += static(
    settings.MEDIA_URL,
    document_root=settings.MEDIA_ROOT
)
```
続いてblogApp側の設定を入れ込んでいきます。
まずはurls.py。Webサーバを稼働させる時にエラーが発生しないように適当に設定しています。
```blogApp/urls.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path("admin/", admin.site.urls),
]
```
続いてmodels.py
ブログ記事に必要となるタイトル、カテゴリ、作成者、作成日時、更新日時を設定できるポストモデルを用意しました。
また、ブログ記事のカテゴリを作成するためのカテゴリモデルも用意しています。
```blogApp/models.py
from django.db import models
from markdownx.models import MarkdownxField
from django.utils import timezone
from django.contrib.auth.models import User

class Category(models.Model):
    name = models.CharField(max_length=100)

    def __str__(self):
        return self.name

class Post(models.Model):
    title = models.CharField(max_length=200)  
    content = MarkdownxField()  
    category = models.ForeignKey(Category, on_delete=models.CASCADE, blank=True, null=True)  
    author = models.ForeignKey(User, on_delete=models.CASCADE)  
    created_at = models.DateTimeField(default=timezone.now)  
    updated_at = models.DateTimeField(auto_now=True)  

    def __str__(self):
        return self.title
```
次にadmin.pyとなります。
これを設定することでadmin管理画面でmodels.pyで作成したmodelが見れるようになります。
markdown記法をするために対象となるポストモデルに対しては"register(Post, MarkdownxModelAdmin)"と少々変わった書き方をしています。
```blogApp/admin.py
from django.contrib import admin
from .models import *
from markdownx.admin import MarkdownxModelAdmin

admin.site.register(Category)
admin.site.register(Post, MarkdownxModelAdmin)
```

## makemigrationsとmigrate、adminユーザ作成
アプリ側にあるmodels.pyをデータベースに反映していきます。
makemigrationsで警告が出ていますが、これはおそらくプロジェクトとアプリのurls.pyで両方adminのURLを定義しているので出てきているのだと思います。ここでは無視します。
```
PS C:\Users\ohtsu\Documents\DjangoEnv\django-dev\blogPJ> python manage.py makemigrations
System check identified some issues:
WARNINGS:
?: (urls.W005) URL namespace 'admin' isn't unique. You may not be able to reverse all URLs in this namespace
Migrations for 'blogApp':
  blogApp\migrations\0001_initial.py
    - Create model Category
    - Create model Post

PS C:\Users\ohtsu\Documents\DjangoEnv\django-dev\blogPJ> python manage.py migrate
```
続いてadmin管理画面にログインするためのadminユーザを作成します。
```
PS C:\Users\ohtsu\Documents\DjangoEnv\django-dev\blogPJ> python manage.py createsuperuser
System check identified some issues:
WARNINGS:
?: (urls.W005) URL namespace 'admin' isn't unique. You may not be able to reverse all URLs in this namespace
ユーザー名 (leave blank to use 'ohtsu'): admin
メールアドレス: admin@example.com
Password: 
Password (again): 
このパスワードは一般的すぎます。
Bypass password validation and create user anyway? [y/N]: y
Superuser created successfully.
```
runserverしていきます
```
PS C:\Users\ohtsu\Documents\DjangoEnv\django-dev\blogPJ> python manage.py runserver 0.0.0.0:8000
Watching for file changes with StatReloader
Performing system checks...

System check identified some issues:

WARNINGS:
?: (urls.W005) URL namespace 'admin' isn't unique. You may not be able to reverse all URLs in this namespace

System check identified 1 issue (0 silenced).
February 18, 2024 - 11:27:35
Django version 4.1, using settings 'blogPJ.settings'
Starting development server at http://0.0.0.0:8000/
Quit the server with CTRL-BREAK.
[18/Feb/2024 11:27:44] "GET /admin/ HTTP/1.1" 302 0
[18/Feb/2024 11:27:44] "GET /admin/login/?next=/admin/ HTTP/1.1" 200 2251
```

## markdownで書けるか確認する
http\://127.0.0.1:8000/adminにWebブラウザ経由でアクセスしてadminユーザで管理画面にログインします。
今回はPostsにアクセスします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3219385/5d5d8dd0-e252-0c7e-a2fd-7bab2e3f1dcd.png)
右上にあるPOSTを追加を押下します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3219385/c3a8e7c4-cc95-6c1f-1491-b9fe26f9e8a1.png)
今回はこんな感じで作成してみました。
赤枠のところは本来markdown記法で記載できないのですが、django-markdownxをインストールし環境を設定することでmarkdown記法で記載出来ています。
そしてオレンジ枠の部分でプレビューが表示されております。これを横に並列させることもできるっぽいので今後実装していきたいところ。
右下の保存を押下します。
![2024021802.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3219385/6978aeec-5c6b-27d2-50c7-762c9c9f9dc4.png)
作成できていますね。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3219385/323a81b2-3526-1c11-d004-1a62c58ea3dc.png)
sqlite3上にデータが存在することを確認します。このタイミングではまだHTML的な感じではないのですね。今後検証ですね。
```
PS C:\Users\ohtsu\Documents\DjangoEnv\django-dev\blogPJ> sqlite3 .\db.sqlite3
SQLite version 3.41.2 2023-03-22 11:56:21
Enter ".help" for usage hints.

sqlite> .tables
auth_group                  blogApp_category
auth_group_permissions      blogApp_post
auth_permission             django_admin_log
auth_user                   django_content_type       
auth_user_groups            django_migrations
auth_user_user_permissions  django_session

sqlite> SELECT * FROM blogApp_post;
2|test|# test title
## test subtitle

test content 

![test picture](/media/markdownx/1eefb8ef-5140-4ce8-8a98-72bfd9d11e91.png)|2024-02-18 05:29:23|2024-02-18 05:42:58.926455|1|
```