作成: 2019年02月25日
更新: 2021年06月11日
Djangoで作ったWebアプリをラズベリーパイで起動させる
でDjangoで作ったアプリを公開することができたので、ブログを作ってみたくなった。
しかし記事を書くたびにサーバーをいじるのは面倒なのでQiitaのようにmarkdownで記事を書き、それを自身のサイトにアップロードすることで記事を追加できるようにしたい。
誰でも投稿できては困るので投稿フォームにはDjangoの管理者用ユーザーネームとパスワードを用いてのみアクセスできるようにする。
投稿フォームではタイトル、カテゴリー、markdownファイル、必要な画像ファイルを入力し、それらと投稿時の日時をデータベースに保存する。ファイルの保存場所はデータベース保存時に自動生成されるidをディレクトリ名とし、そこに保存する。
保存された記事を閲覧するときはmarkdownをmarked.jsとhighlight.jsを用いてパースし、表示する。
投稿フォームはDjangoの機能を使って作成する。アプリケーションディレクトリにforms.pyを作成し、以下のようにする。
Articleモデルに関しては後述する。
forms.pyfrom django import forms
from .models import Article
class ArticleForm(forms.ModelForm):
image = forms.ImageField(required=False, widget=forms.ClearableFileInput(attrs={'multiple': True}))
class Meta:
model = Article
fields = ('title', 'article', 'category_split_space',)
imageは画像を受け取るフォームである。画像なしでも問題なくするためにrequired=Falseとし、複数の画像を渡せるようにforms.ClearableFileInput(attrs={'multiple': True}))とする。class Meta以下はArticleモデル(後述)のfieldをフォームとして使用することを表している。
注意点としてImageFieldを用いる際にはPillowというライブラリが必要なのでインストールしていない場合はpipでインストールしておく。
pip install Pillow
このformをview.pyで以下のように作成してtemplateに渡す。
view.pydef post(request):
articleForm = ArticleForm()
return render(request, "post.html", {"articleForm": articleForm})
templateでは以下のようにすることでformが生成される。
post.html<form id="postForm" method="POST" action="/blog/posted" enctype="multipart/form-data">
{{ articleForm.as_p }}
{% csrf_token %}
<button type="submit">投稿</button>
</form>
投稿された記事のデータを保存するためのモデルは以下のように定義する。
models.pyfrom django.db import models
from django.db.models import CharField, FileField, DateTimeField, ManyToManyField, ImageField
from django.utils import timezone
from django.dispatch import receiver
from django.db.models.signals import post_save, pre_save
import os.path
# Create your models here.
class Category(models.Model):
name = CharField(max_length=128)
def md_file_path(instance, filename):
fn, ext = os.path.splitext(filename)
return "article/{id}/{id}{ext}".format(id=instance.pk, ext=ext)
class Article(models.Model):
title = CharField(max_length=128)
article = FileField(upload_to=md_file_path)
post_date = DateTimeField(default=timezone.now)
#スペース区切りのカテゴリー
category_split_space = CharField(max_length=128)
category = ManyToManyField(Category)
#articleの保存ディレクトリ名にprimary keyを使うため
_UNSAVED_FILEFIELD = 'unsaved_filefield'
@receiver(pre_save, sender=Article)
def skip_saving_file(sender, instance, **kwargs):
if not instance.pk and not hasattr(instance, _UNSAVED_FILEFIELD):
setattr(instance, _UNSAVED_FILEFIELD, instance.article)
instance.article = None
@receiver(post_save, sender=Article)
def save_file(sender, instance, created, **kwargs):
if created and hasattr(instance, _UNSAVED_FILEFIELD):
instance.article = getattr(instance, _UNSAVED_FILEFIELD)
instance.save()
instance.__dict__.pop(_UNSAVED_FILEFIELD)
Articleのfieldを一つずつ説明する。
Categoryモデルはカテゴリー名だけをfieldに持つモデルであり、Articleとリレーションをはっている。
md_file_pathはmarkdownファイルの保存場所を指定するものであり
"プロジェクトディレクトリ"/media/article/"articleモデルのid"/"articleモデルのid".md
という風に保存される。ディレクトリ名、ファイル名に自動生成されるモデルのidを用いることで後で参照しやすくなる。
しかしこれだけだとモデル保存前(モデルのid生成前)にidを参照しようとしているためidの部分がNoneになってしまう。この問題を回避するためにskip_saving_file、save_fileという2つの関数でモデル保存時の挙動を変更した。
Djangoでファイルアップロードの際にファイル名にprimary keyを使う方法
またmarkdownファイルはmediaディレクトリに保存されるためサーバーで設定が必要。Djangoのデバッグ用サーバーでは以下のように設定。
settings.pyMEDIA_ROOT = os.path.join(BASE_DIR, 'media')
MEDIA_URL = '/media/'
そしてformから受け取った情報はviews.pyで以下のように処理する。
views.pydef posted(request):
form = ArticleForm(request.POST, request.FILES)
categories = Category.objects.all()
if form.is_valid():
article = form.save()
os.makedirs(settings.BASE_DIR + "/media/article/" + str(article.id) + "/image")
for image in request.FILES.getlist("image"):
with open(settings.BASE_DIR + "/media/article/" + str(article.id) + "/image/" + image.name, "wb+") as destination:
for chunk in image.chunks():
destination.write(chunk)
category_list = article.category_split_space.split()
for c in category_list:
#新規カテゴリーを作成
if len(categories.filter(name=c)) == 0:
new_category = Category(name=c)
new_category.save()
article.category.add(new_category)
else:
category = categories.filter(name=c)[0]
article.category.add(category)
article.save()
articles = Article.objects.all()
return render(request, "blog.html", {"articles": articles})
以下の部分でformの画像ファイルを保存している。
まずos.makedirsでmedia/article/"記事のid"ディレクトリにimageディレクトリを作りそこにフォームから受け取った画像ファイルを書き込んでいる。
views.pyos.makedirs(settings.BASE_DIR + "/media/article/" + str(article.id) + "/image")
for image in request.FILES.getlist("image"):
with open(settings.BASE_DIR + "/media/article/" + str(article.id) + "/image/" + image.name, "wb+") as destination:
for chunk in image.chunks():
destination.write(chunk)
このままではだれでも投稿できるのでviews.pyのpost、postedメソッドに@login_requiredというアノテーションをつけ、superuserによるログインを必須にする。
views.py@login_required
def post(request):
articleForm = ArticleForm()
return render(request, "post.html", {"articleForm": articleForm})
@login_required
def posted(request):
form = ArticleForm(request.POST, request.FILES)
categories = Category.objects.all()
if form.is_valid():
article = form.save()
os.makedirs(settings.BASE_DIR + "/media/article/" + str(article.id) + "/image")
for image in request.FILES.getlist("image"):
with open(settings.BASE_DIR + "/media/article/" + str(article.id) + "/image/" + image.name, "wb+") as destination:
for chunk in image.chunks():
destination.write(chunk)
category_list = article.category_split_space.split()
for c in category_list:
#新規カテゴリーを作成
if len(categories.filter(name=c)) == 0:
new_category = Category(name=c)
new_category.save()
article.category.add(new_category)
else:
category = categories.filter(name=c)[0]
article.category.add(category)
article.save()
articles = Article.objects.all()
return render(request, "blog.html", {"articles": articles})
superuser、loginの設定は下記参照
Django2 でユーザー認証(ログイン認証)を実装するチュートリアル -2- サインアップとログイン・ログアウト | ITエンジニアラボ
Using the Django authentication system | Django documentation | Django
marked.jsとhilight.jsをCDNなどでダウンロードする。
<!--CSS-->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.14.2/styles/vs2015.min.css">
<!--JavaScript-->
<script src="https://code.jquery.com/jquery-3.3.1.min.js" integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8="
crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js" integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49"
crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/marked/0.6.0/marked.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.14.2/highlight.min.js"></script>
保存されたmarkdownファイルをmarked.jsとhilight.jsを用いて以下のようにhtmlへと変換している。
ところどころ装飾にbootstrapを用いている。
view.html{% extends "base.html" %}
{% block title%}
{{ article.title }}--banaTECH--
{% endblock %}
{% block content %}
<div class="container">
<h1>{{ article.title }}</h1>
<h2>{{ article.post_date }}</h2>
<p>
カテゴリー:
{% for category in article.category.all%}
<a class="btn btn-primary" href="/blog/category/{{ category.name }}">{{ category.name }}</a>
{% endfor %}
</p>
<div id="markdown_content" src="{{article.article.url}}"></div>
</div>
<script>
$(document).ready(function () {
var target = $("#markdown_content");
var renderer = new marked.Renderer()
renderer.code = function (code, language) {
if(language.indexOf(":") != -1){
var lang = language.split(":")[0];
var fileName = language.split(":")[1].trim();
return '<pre>' + '<div class="card text-white bg-info" style="display: inline-block;"> <div class="card-body p-0" style="display: inline-block;">' + fileName + ' ' + '</div></div>' + '<code class="hljs">' + hljs.highlightAuto(code, [lang]).value + '</code></pre>';
}else{
return '<pre' + '><code class="hljs">' + hljs.highlightAuto(code).value + '</code></pre>';
}
};
renderer.image = function (href, title, text) {
var fileName = href.split("/").pop();
return '<img src="/media/article/{{ article.id }}/image/'+ fileName +'" alt="'+ text +'" class="img-fluid">';
}
marked.setOptions({
renderer: renderer,
sanitize: true,
breaks: true
});
$.ajax({
url: target[0].attributes["src"].value
})
.then(
function (data) {
target.append(marked(data));
},
function () {
target.append("This content failed to load.");
}
);
});
</script>
{% endblock %}
<div id="markdown_content" src="{{article.article.url}}"></div>
の部分がmarkdownで書かれた記事になる。
以下の部分でQiitaのように:で区切ればファイル名を表示できるようにしている(bootstrapで装飾)
view.htmlrenderer.code = function (code, language) {
if(language.indexOf(":") != -1){
var lang = language.split(":")[0];
var fileName = language.split(":")[1].trim();
return '<pre>' + '<div class="card text-white bg-info" style="display: inline-block;"> <div class="card-body p-0" style="display: inline-block;">' + fileName + ' ' + '</div></div>' + '<code class="hljs">' + hljs.highlightAuto(code, [lang]).value + '</code></pre>';
}else{
return '<pre' + '><code class="hljs">' + hljs.highlightAuto(code).value + '</code></pre>';
}
};
以下の部分で画像のパスを修正している。ファイル名は保存してあるものと同じものにする必要があり。
view.htmlrenderer.image = function (href, title, text) {
var fileName = href.split("/").pop();
return '<img src="/media/article/{{ article.id }}/image/'+ fileName +'" alt="'+ text +'" class="img-fluid">';
}
以下の部分でcodeとimageの設定を適用し、ついでにサニタイズとbrタグによる改行をオンにする。
view.htmlmarked.setOptions({
renderer: renderer,
sanitize: true,
breaks: true
});
CDNの以下の部分を変更することでソースコードのシンタックスハイライトのテーマを変更することができる。
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.14.2/styles/vs2015.min.css">
テーマ一覧
highlight.js demo
最終的に以下のようQiitaライクのブログを作れた。もう少しデザインや機能を追加したら公開したい。
現在はほぼ同じ方法を使ってLaravelに移行しています(このページ)
Djangoでのブログに関するコード(github)
https://github.com/bana118/banatech_django/tree/master/banaTECH/blog