naritoブログ

【お知らせ】
新ブログができました。今後そちらで更新し、このサイトは更新されません(ウェブサイト自体は残しておきます)
このブログの内容に関してコメントしたい場合は、新ブログのフリースペースに書き込んでください

このブログの内容を新ブログに移行中です。このブログで見つからない記事は、新ブログにありま

DjangoとBootstrap4で、ブログを作る

約806日前 2016年9月26日22:38
プログラミング関連
Bootstrap4 Django Python
Githubにソースをおいています。この記事の内容より進んでおり、洗練されています。このブログも、そのGithubのソースコードをそのまま動かしています。


-----------------------------
以下、古いブログのソースです。
----------------------------

トップページ


右のカテゴリは、大カテゴリです。クリックするとPython...などの小カテゴリが開きます。


押すと、そのカテゴリでの記事一覧になります。タグも同様です。


上のナビバーにある検索フォームは、クイックサーチ的なものです。


続きを読むを押すと、記事の詳細画面に。カテゴリ、タグの最新5件なんかも表示されています。


とりあえずは、投稿はadminで行います。カテゴリの追加等も同様です。


↓にあるHTMLソースか?にチェックをするとHTMLを書くことができ


ちゃんと反映されますね。


Django1.10
Python3.5
プロジェクト名「blog」
アプリケーション名「torina_blog」
です。

blog/blog/settings.py
INSTALLERD_APPSの追加と、MEDIA_URL等の設定、
そしてcontext_processorsに自作のものを足します。

INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'torina_blog', # 足した
]
...
...
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'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',
'torina_blog.context_processors.common', # 足した
],
},
},
]
...
...
# 足した
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
MEDIA_URL = "/media/"


blog/blog/urls.py
開発環境での/media/の紐づけと、adminと作ったアプリのurls.pyを読み込む

from django.conf import settings
from django.conf.urls import url, include
from django.conf.urls.static import static
from django.contrib import admin

urlpatterns = [
url(r'^admin/', admin.site.urls),
url(r'^', include('torina_blog.urls', namespace='torina_blog')),
]
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL,
document_root=settings.MEDIA_ROOT)


blog/torina_blog/urls.py
カテゴリについて、
/category/大カテゴリ
/category/大カテゴリ/小カテゴリ
のようになります。大カテゴリの検索と、小カテゴリの検索ですね。

from django.conf.urls import url
from . import views


urlpatterns = [
url(r'^$', views.PostIndexView.as_view(), name='index'),

url(r'^detail/(?P<pk>[0-9]+)/$',
views.PostDetailView.as_view(), name='detail'),

url(r'^category/(?P<big>\w+)/(?P<small>\w+)/$',
views.CategoryView.as_view(), name='category'),

url(r'^category/(?P<big>\w+)/$',
views.CategoryView.as_view(), name='category'),

url(r'^tag/(?P<tag>\w+)/$',
views.TagView.as_view(), name='tag'),
]


タグ名やカテゴリ名によっては、

Reverse for 'category' with arguments

というようなエラーが出るかもしれません。その場合は、下記のように「.*」を使いましょう。

url(r'^category/(?P<big>.*)/$',
views.CategoryView.as_view(), name='category'),


blog/torina_blog/admin.py
adminで編集できるよう、追加します。

from django.contrib import admin
from .models import Post, SmallCategory, BigCategory, Tag

admin.site.register(Post)
admin.site.register(SmallCategory)
admin.site.register(BigCategory)
admin.site.register(Tag)



blog/torina_blog/context_processors.py
このアプリでは、どの画面に行っても大カテゴリの一覧とタグが存在しています。
このような場合、全てのviewでその処理を書くのではなく、context_processorを使います。

from .models import BigCategory, Tag


def common(request):
context = {
"big_categories": BigCategory.objects.all(),
"tags": Tag.objects.all(),
}
return context


やってることは、単純に大カテゴリとタグを全て取得しているだけです。
これをsettings.pyのcontext_processorsに足すことで、どのtemplateからでも参照できます。


blog/torina_blog/forms.py
今回forms.pyは使いませんでした。

blog/torina_blog/models.py

from django.db import models


def _get_latest_post(queryset):
"""もらったクエリセットを、さらに絞り込んで返す。"""

return queryset.filter(is_publick=True).order_by('-created_at')[:5]


class BigCategory(models.Model):
"""大カテゴリ"""

name = models.CharField("大カテゴリ名", max_length=255)
created_at = models.DateTimeField("作成日", auto_now_add=True)

def __str__(self):
return self.name

def get_latest_post(self):
queryset = Post.objects.filter(category__parent=self)
return _get_latest_post(queryset)


class SmallCategory(models.Model):
"""小カテゴリー"""

name = models.CharField("小カテゴリ名", max_length=255)
parent = models.ForeignKey(BigCategory, verbose_name="大カテゴリ")
created_at = models.DateTimeField("作成日", auto_now_add=True)

def __str__(self):
return self.name

def get_latest_post(self):
queryset = Post.objects.filter(category=self)
return _get_latest_post(queryset)


class Tag(models.Model):
"""タグ"""

name = models.CharField("タグ名", max_length=255)
created_at = models.DateTimeField("作成日", auto_now_add=True)

def __str__(self):
return self.name

def get_latest_post(self):
queryset = Post.objects.filter(tag=self)
return _get_latest_post(queryset)


class Post(models.Model):
"""ブログのポスト"""

title = models.CharField("タイトル", max_length=255)
text = models.TextField("本文")
category = models.ForeignKey(SmallCategory, verbose_name="カテゴリ")
tag = models.ManyToManyField(Tag, blank=True, verbose_name="タグ")
thumbnail = models.ImageField("サムネイル", upload_to='thumnail/', blank=True)
is_publick = models.BooleanField("公開可能か?", default=True)
is_html = models.BooleanField("HTMLソースか?", default=False)
created_at = models.DateTimeField("作成日", auto_now_add=True)
updated_at = models.DateTimeField("更新日", auto_now=True)

def __str__(self):
return self.title


Django、ブログに使っているModel
https://torina.top/detail/256/
の内容とほとんど同じです。

まずこれですが、ブログの記事にはis_publicというフィールドがあります。
また、記事の作成日降順で表示しますが、これらは共通です。
以下の関数は、その共通の部分だけを行うヘルパー関数です。

def _get_latest_post(queryset):
"""もらったクエリセットを、さらに絞り込んで返す。"""

return queryset.filter(is_publick=True).order_by('-created_at')[:5]



記事は見たまんまです。

class Post(models.Model):
"""ブログのポスト"""

title = models.CharField("タイトル", max_length=255)
text = models.TextField("本文")
category = models.ForeignKey(SmallCategory, verbose_name="カテゴリ")
tag = models.ManyToManyField(Tag, blank=True, verbose_name="タグ")
thumbnail = models.ImageField("サムネイル", upload_to='thumnail/', blank=True)
is_publick = models.BooleanField("公開可能か?", default=True)
is_html = models.BooleanField("HTMLソースか?", default=False)
created_at = models.DateTimeField("作成日", auto_now_add=True)
updated_at = models.DateTimeField("更新日", auto_now=True)

def __str__(self):
return self.title




get_latest_postは、そのタグやカテゴリでの最新5件を返すメソッドです。
templateから呼び出して使います。
カテゴリやタグ等の絞り込みまでは行い、後はis_public等の共通の処理を行う_get_lates_postに渡します。

class BigCategory(models.Model):
"""大カテゴリ"""

name = models.CharField("大カテゴリ名", max_length=255)
created_at = models.DateTimeField("作成日", auto_now_add=True)

def __str__(self):
return self.name

def get_latest_post(self):
queryset = Post.objects.filter(category__parent=self)
return _get_latest_post(queryset)


class SmallCategory(models.Model):
"""小カテゴリー"""

name = models.CharField("小カテゴリ名", max_length=255)
parent = models.ForeignKey(BigCategory, verbose_name="大カテゴリ")
created_at = models.DateTimeField("作成日", auto_now_add=True)

def __str__(self):
return self.name

def get_latest_post(self):
queryset = Post.objects.filter(category=self)
return _get_latest_post(queryset)


class Tag(models.Model):
"""タグ"""

name = models.CharField("タグ名", max_length=255)
created_at = models.DateTimeField("作成日", auto_now_add=True)

def __str__(self):
return self.name

def get_latest_post(self):
queryset = Post.objects.filter(tag=self)
return _get_latest_post(queryset)



blog/torina_blog/views.py

from django.core.urlresolvers import reverse_lazy
from django.db.models import Q
from django.views import generic
from .models import Post


class BaseListView(generic.ListView):
paginate_by = 10

def base_queryset(self):
queryset = Post.objects.filter(
is_publick=True).order_by('-created_at')
return queryset


class PostIndexView(BaseListView):

def get_queryset(self):
queryset = self.base_queryset()
keyword = self.request.GET.get("quick")
if keyword:
queryset = queryset.filter(
Q(title__icontains=keyword) | Q(text__icontains=keyword))

return queryset


class CategoryView(BaseListView):

def get_queryset(self):
queryset = self.base_queryset()
category = self.kwargs.get("small")
if category:
queryset = queryset.filter(category__name=category)
else:
category = self.kwargs.get("big")
queryset = queryset.filter(category__parent__name=category)

return queryset


class TagView(BaseListView):

def get_queryset(self):
tag = self.kwargs["tag"]
queryset = self.base_queryset().filter(tag__name=tag)
return queryset


class PostDetailView(generic.DetailView):
model = Post


paginate_by = 10は10件づつの表示、base_querysetはis_publicがTrue、作成日降順で絞り込みます。
一覧表示する他のViewはこいつを継承して使います。

class BaseListView(generic.ListView):
paginate_by = 10

def base_queryset(self):
queryset = Post.objects.filter(
is_publick=True).order_by('-created_at')
return queryset


/でアクセスする一覧表示のビュー。上記のBaseListViewを継承しています。

class PostIndexView(BaseListView):

def get_queryset(self):
queryset = self.base_queryset()
keyword = self.request.GET.get("quick")
if keyword:
queryset = queryset.filter(
Q(title__icontains=keyword) | Q(text__icontains=keyword))

return queryset


self.base_queryset()で表示する一覧データを作成しますが、ナビバーにあったクイックサーチに何か入力されていた場合は以下の処理を行います。
Qオブジェクトは、OR検索に使います。containsだと大文字小文字も区別し、icontainsは区別しません。
タイトルかテキストに、入力された文字(大文字小文字区別しない)が含まれているものを返します。

keyword = self.request.GET.get("quick")
if keyword:
queryset = queryset.filter(
Q(title__icontains=keyword) | Q(text__icontains=keyword))


カテゴリ検索です。小カテゴリ、大カテゴリ兼用です。

class CategoryView(BaseListView):

def get_queryset(self):
queryset = self.base_queryset()
category = self.kwargs.get("small")
if category:
queryset = queryset.filter(category__name=category)
else:
category = self.kwargs.get("big")
queryset = queryset.filter(category__parent__name=category)

return queryset



こっちはタグ検索。

class TagView(BaseListView):

def get_queryset(self):
tag = self.kwargs["tag"]
queryset = self.base_queryset().filter(tag__name=tag)
return queryset


詳細画面。何も言うことはない。

class PostDetailView(generic.DetailView):
model = Post



blog/torina_blog/templates/torina_blog/base.html

{% load static %}
<!DOCTYPE html>
<html lang="ja">
<head>
<!-- Required meta tags always come first -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta http-equiv="x-ua-compatible" content="ie=edge">

<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.4/css/bootstrap.min.css" integrity="2hfp1SzUoho7/TsGGGDaFdsuuDL0LX2hnUp6VkX3CUQ2K4K+xjboZdsXyp4oUHZj" crossorigin="anonymous">

<!-- Custom CSS -->
<link rel="stylesheet" href="{% static 'torina_blog/css/base.css' %}">

</head>
<body>
<nav class="navbar navbar-dark bg-primary">
<button class="navbar-toggler hidden-sm-up" type="button" data-toggle="collapse" data-target="#exCollapsingNavbar2">
&#9776;
</button>
<div class="collapse navbar-toggleable-xs" id="exCollapsingNavbar2">
<div class="container">
<a class="navbar-brand" href="#">Blog Name</a>
<ul class="nav navbar-nav">
<li class="nav-item {% block nav_home %}{% endblock %}">
<a class="nav-link" href="{% url 'torina_blog:index' %}">Home <span class="sr-only">(current)</span></a>
</li>
{% if user.is_authenticated %}
<li class="nav-item">
<a class="nav-link" href="{% url 'admin:index' %}">Admin</a>
</li>
{% endif %}
</ul>
<form action="{% url 'torina_blog:index' %}" method="GET" class="form-inline pull-xs-right">
<input class="form-control" type="text" name="quick" placeholder="Search">
<button class="btn btn-outline-success" type="submit">Search</button>
</form>
</div>
</div>
</nav>

<div class="jumbotron">
<div class="container">
<h1>Django + Bootstrap4 Blog</h1>
<p class="lead text-muted">DjangoとBootstrap4で作成されました</p>
</div>
</div>

<div class="container">
<div class="row">
<div class="col-xs-12 col-sm-8">
{% block content %}{% endblock %}
</div>
<div class="col-xs-12 col-sm-3 offset-sm-1">
{% include "torina_blog/side.html" %}
</div>
</div>
</div>
<!-- jQuery first, then Tether, then Bootstrap JS. -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.0.0/jquery.min.js" integrity="sha384-THPy051/pYDQGanwU6poAc/hOdQxjnOEXzbT+OuUAFqNqFjL+4IGLBgCJC3ZOShY" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/tether/1.2.0/js/tether.min.js" integrity="sha384-Plbmg8JY28KFelvJVai01l8WyZzrYWG825m+cZ0eDDS1f7d/js6ikvy1+X+guPIB" crossorigin="anonymous"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.4/js/bootstrap.min.js" integrity="VjEeINv9OSwtWFLAtmc4JCtEJXXBub00gtSnszmspDLCtC0I4z4nqz7rEFbIZLLU" crossorigin="anonymous"></script>

<!-- Custom JS -->
<script src="{% static 'torina_blog/js/base.js' %}"></script>
</body>
</html>


これはナビバーです。入力フォーム付きです。そしてレスポンシブです。
ログインしている場合は、管理画面へのリンクなんかもつけています。

<nav class="navbar navbar-dark bg-primary">
<button class="navbar-toggler hidden-sm-up" type="button" data-toggle="collapse" data-target="#exCollapsingNavbar2">
&#9776;
</button>
<div class="collapse navbar-toggleable-xs" id="exCollapsingNavbar2">
<div class="container">
<a class="navbar-brand" href="#">Blog Name</a>
<ul class="nav navbar-nav">
<li class="nav-item {% block nav_home %}{% endblock %}">
<a class="nav-link" href="{% url 'torina_blog:index' %}">Home <span class="sr-only">(current)</span></a>
</li>
{% if user.is_authenticated %}
<li class="nav-item">
<a class="nav-link" href="{% url 'admin:index' %}">Admin</a>
</li>
{% endif %}
</ul>
<form action="{% url 'torina_blog:index' %}" method="GET" class="form-inline pull-xs-right">
<input class="form-control" type="text" name="quick" placeholder="Search">
<button class="btn btn-outline-success" type="submit">Search</button>
</form>
</div>
</div>
</nav>


スマホでは、以下のような表示になります。


blog/torina_blog/static/torina_blog/base.css
カスタムcss、jsの読み込みなんか書いてましたが、jsは特に書くものはありませんでした。
cssもほとんど空で、内容は以下です。
jumbotronの背景を白くし、下側に線を引いただけです。

div.jumbotron {
background-color: #fff;
border-bottom: .05rem solid #eee;
}


Bootstrap3とは違い、col-sm-offsetではなくoffset-smになりました。
サイドバーは長くなったため、別HTMLにしました。

<div class="container">
<div class="row">
<div class="col-xs-12 col-sm-8">
{% block content %}{% endblock %}
</div>
<div class="col-xs-12 col-sm-3 offset-sm-1">
{% include "torina_blog/side.html" %}
</div>
</div>
</div>


blog/torina_blog/templates/torina_blog/side.html

<div class="list-group">
<a href="#" class="list-group-item active text-xs-center">
All Category
</a>
{% for big in big_categories %}
<a class="list-group-item text-xs-center" data-toggle="collapse" href="#c-{{ big.pk }}" aria-expanded="false" aria-controls="c-{{ big.pk }}">
{{ big.name }}
</a>
<div class="collapse" id="c-{{ big.pk }}">
<a href="{% url 'torina_blog:category' big.name %}" class="list-group-item">
{{ big.name }}
</a>
{% for small in big.smallcategory_set.all %}
<a href="{% url 'torina_blog:category' big.name small.name %}" class="list-group-item">{{ small.name }}</a>
{% endfor %}
</div>
{% endfor %}
</div>



<div class="list-group m-t-3">
<a href="#" class="list-group-item active text-xs-center">
Tag
</a>
<div class="list-group-item">
{% for tag in tags %}
<a href="{% url 'torina_blog:tag' tag.name %}">{{ tag.name }}</a>,
{% endfor %}
</div>
</div>


サイドバーなので、以下の画像の部分ですね。


これはカテゴリの一覧です。
真ん中寄せには、text-xs-centerが便利です。xsとありますが、smにしたりサイズによって分けれます。わーお

<div class="list-group">
<a href="#" class="list-group-item active text-xs-center">
All Category
</a>
{% for big in big_categories %}
<a class="list-group-item text-xs-center" data-toggle="collapse" href="#c-{{ big.pk }}" aria-expanded="false" aria-controls="c-{{ big.pk }}">
{{ big.name }}
</a>
<div class="collapse" id="c-{{ big.pk }}">
<a href="{% url 'torina_blog:category' big.name %}" class="list-group-item">
{{ big.name }}
</a>
{% for small in big.smallcategory_set.all %}
<a href="{% url 'torina_blog:category' big.name small.name %}" class="list-group-item">{{ small.name }}</a>
{% endfor %}
</div>
{% endfor %}
</div>


上の内容は、例えば以下のようなHTMLになります。
data-toggle="collapse" とし、hrefに指定したIDを持つ要素が表示される、ということですね。

<div class="list-group">
<a href="#" class="list-group-item active text-xs-center">
All Category
</a>

<a class="list-group-item text-xs-center" data-toggle="collapse" href="#c-1" aria-expanded="false" aria-controls="c-1">
プログラミング言語
</a>
<div class="collapse" id="c-1">
<a href="/category/%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0%E8%A8%80%E8%AA%9E/" class="list-group-item">
プログラミング言語
</a>

<a href="/category/%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0%E8%A8%80%E8%AA%9E/Python/" class="list-group-item">Python</a>

<a href="/category/%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0%E8%A8%80%E8%AA%9E/Ruby/" class="list-group-item">Ruby</a>

<a href="/category/%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0%E8%A8%80%E8%AA%9E/Perl/" class="list-group-item">Perl</a>

<a href="/category/%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0%E8%A8%80%E8%AA%9E/PHP/" class="list-group-item">PHP</a>

<a href="/category/%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0%E8%A8%80%E8%AA%9E/C/" class="list-group-item">C</a>

<a href="/category/%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0%E8%A8%80%E8%AA%9E/Java/" class="list-group-item">Java</a>

</div>

<a class="list-group-item text-xs-center" data-toggle="collapse" href="#c-2" aria-expanded="false" aria-controls="c-2">
OS
</a>
<div class="collapse" id="c-2">
<a href="/category/OS/" class="list-group-item">
OS
</a>

<a href="/category/OS/Windows/" class="list-group-item">Windows</a>

<a href="/category/OS/Mac/" class="list-group-item">Mac</a>

<a href="/category/OS/Unix%E7%B3%BB/" class="list-group-item">Unix系</a>

<a href="/category/OS/Linux%E7%B3%BB/" class="list-group-item">Linux系</a>

</div>

</div>



templateには大カテゴリが渡されている(context_processeors)ので、以下のようにして大カテゴリに紐づく小カテゴリが取得できます。
今回のカテゴリに限らず、よく使う書き方です。

{% for big in big_categories %}
{% for small in big.smallcategory_set.all %}


こっちはタグです。面倒くさくなり、見た目は妥協しました。
tagsもcontext_processorsで渡していましたね。

<div class="list-group m-t-3">
<a href="#" class="list-group-item active text-xs-center">
Tag
</a>
<div class="list-group-item">
{% for tag in tags %}
<a href="{% url 'torina_blog:tag' tag.name %}">{{ tag.name }}</a>,
{% endfor %}
</div>
</div>


m-t-3ですが、これはmargin-topをしてくれます。数字は間隔の大きさで、m-t-0ならmargin-top: 0です。便利ですね。
p-t-1ならpadding-topになりますし、m-l-1ならmarin-left、詳しくは以下のページに載っています。
https://v4-alpha.getbootstrap.com/components/utilities/
現在は、mt-3 等のようにリネームされたようですね。


blog/torina_blog/templates/torina_blog/post_list.html
一覧画面です。

{% extends "torina_blog/base.html" %}
{% load static %}
{% block nav_home %}active{% endblock %}
{% block content %}
{% for post in post_list %}
<div class="row m-b-3">
<div class="col-xs-12 col-sm-6 text-xs-center">
{% if post.thumbnail %}
<img class="img-thumbnail" src="{{ post.thumbnail.url }}">
{% else %}
<img class="img-thumbnail" src="{% static 'torina_blog/img/noimage.png' %}">
{% endif %}
</div>
<div class="col-xs-12 col-sm-6">
<h4>{{ post.title }}</h4>
<span class="tag tag-info">{{ post.category.parent }}</span>
<span class="tag tag-primary">{{ post.category }}</span>
{% for tag in post.tag.all %}
<span class="tag tag-success">{{ tag.name }}</span>
{% endfor %}
<p class="text-muted">{{ post.created_at }}</p>
{% if user.is_authenticated %}
<p>
<a class="nav-link" href="{% url 'admin:torina_blog_post_change' post.pk %}">管理画面へ</a>
</p>
{% endif %}
<a class="btn btn-outline-primary btn-lg btn-block" href="{% url 'torina_blog:detail' post.pk %}">続きを読む</a>
</div>
</div>
{% endfor %}

<div class="text-xs-center">
{% include "torina_blog/page.html" %}
</div>
{% endblock %}


m-b-3は、margin-bottomで下に余白をつけています。
img-thumbnailには以下の指定がされています。
max-width:100%、height:auto;あたりは、img要素をレスポンシブにしたい場合によく使う処理です。
width:100%だと、小さい画像は引き延ばされてきたなくなったりします。
これの親要素にtext-xs-centerをしているので、更に中央寄せです。割と汎用的に使えるのではないでしょうか。

.img-thumbnail {
padding: .25rem;
background-color: #fff;
border: 1px solid #ddd;
border-radius: .25rem;
-webkit-transition: all .2s ease-in-out;
-o-transition: all .2s ease-in-out;
transition: all .2s ease-in-out;
display: inline-block;
max-width: 100%;
height: auto;
}


以下は、ログイン済みならその記事の更新画面へのリンクをつけています。

{% if user.is_authenticated %}
<p>
<a class="nav-link" href="{% url 'admin:torina_blog_post_change' post.pk %}">管理画面へ</a>
</p>
{% endif %}


admin管理画面への各種リンクは、以前に書きました。

Django、管理画面へのリンク
https://torina.top/detail/255/

このtagはお洒落なので好きです。tag-pillをつけると丸まります。

<span class="tag tag-info">{{ post.category.parent }}</span>


ページング部分は他HTMLにしています。

<div class="text-xs-center">
{% include "torina_blog/page.html" %}
</div>



blog/torina_blog/templates/torina_blog/page.html
これは他のGETパラメータがあっても正しく動作します。お勧めです。

{% load page %}
<nav aria-label="Page navigation">
<ul class="pagination">

{% if page_obj.has_previous %}
<li class="page-item">
<a class="page-link" href="?{% url_replace request page_obj.previous_page_number %}" aria-label="Previous">
<span aria-hidden="true">&laquo;</span>
</a>
</li>
{% endif %}

{% for link_page in page_obj.paginator.page_range %}
{% if link_page == page_obj.number %}
<li class="page-item active">
<a class="page-link" href="?{% url_replace request link_page %}">
{{ link_page }}
</a>
</li>
{% else %}
<li class="page-item">
<a class="page-link" href="?{% url_replace request link_page %}">
{{ link_page }}
</a>
</li>
{% endif %}
{% endfor %}

{% if page_obj.has_next %}
<li class="page-item">
<a class="page-link" href="?{% url_replace request page_obj.next_page_number %}" aria-label="Next">
<span aria-hidden="true">&raquo;</span>
</a>
</li>
{% endif %}

</ul>
</nav>


そのかわり、少し手間があります。
app、今回ならばtorina_blogディレクトリ配下に「templatetags」ディレクトリを作成します。
その中に、page.pyを作りましょう。このhtmlの冒頭の、{% load page %}のpage部分はこのモジュール名です。
__init__.pyも、同じ階層につくっときましょう。空でいいです。

blog/torina_blog/templatetags/page.py

from django import template

register = template.Library()

@register.simple_tag
def url_replace(request, value):
dict_ = request.GET.copy()
dict_["page"] = str(value) # Django2.1対策。それ以外はvalueだけでOK
return dict_.urlencode()


http://stackoverflow.com/questions/2047622/how-to-paginate-django-with-other-get-variables
からパクりました。他のGETパラメータがあろうが、なかろうが、ちゃんと動くすごいやつです。

どこにディレクトリの作成をするんだ?と悩む場合は、前書いたのを見てもよいです。
Djangoのカスタムフィルタ
https://torina.top/detail/240/



blog/torina_blog/templates/torina_blog/post_detail.html
記事の詳細画面です。

{% extends "torina_blog/base.html" %}
{% block content %}
<div class="card card-outline-primary">
<div class="card-header bg-primary">
<h1 class="card-title">{{ post.title }}</h1>
</div>

<div class="card-block">
{% if post.is_html %}
{{ post.text | safe}}
{% else %}
{{ post.text | urlize | linebreaks }}
{% endif %}

<hr>
{% include "torina_blog/latest_post.html" %}
    
</div>

<div class="card-footer">
<span class="tag tag-info">{{ post.category.parent }}</span>
<span class="tag tag-primary">{{ post.category }}</span>
{% for tag in post.tag.all %}
<span class="tag tag-success">{{ tag.name }}</span>
{% endfor %}
<p class="text-muted">{{ post.created_at }}</p>
{% if user.is_authenticated %}
<p><a class="nav-link" href="{% url 'admin:torina_blog_post_change' post.pk %}">管理画面へ</a></p>
{% endif %}
</div>
</div>
{% endblock %}


cardというものを使用しています。
Bootstrap3のpanelやwell、thumbnailはcardに統合された感じです。
なかなか便利に使えます。

<div class="card card-outline-primary">
<div class="card-header bg-primary">
<h1 class="card-title">{{ post.title }}</h1>
</div>

<div class="card-block">
...
...
</div>

<div class="card-footer">
..
..
</div>
</div>


modelにis_htmlというフィールドがありました。
is_htmlにチェック(True)をいれた場合、{{ post.text | safe}}でHTMLとして解釈されます。
チェックをしていないFalseならば、{{ post.text | urlize | linebreaks }}です。urlizeでurlを<a>タグに変換し、linebreaksで改行を<p>タグ、<br>タグに変換します。
<p>タグがうっとしいな、と思ったらlinebreaksbrで<br>だけにするのも良いでしょう。

{% if post.is_html %}
{{ post.text | safe}}
{% else %}
{{ post.text | urlize | linebreaks }}
{% endif %}



最新5件の表示は、長いので別HTMLにしました。

{% include "torina_blog/latest_post.html" %}



models.pyに定義していたget_latest_postメソッドが使われています。

<div class="row">
<!-- 大カテゴリの最新5件 -->
<div class="col-xs-12 col-sm-6 m-t-1">
「{{ post.category.parent.name }}」の最新5件<br>
{% for latest in post.category.parent.get_latest_post %}
<a href="{% url 'torina_blog:detail' latest.pk %}">{{ latest.title }}</a><br>
{% endfor %}
</div>

<!-- 小カテゴリの最新5件 -->
<div class="col-xs-12 col-sm-6 m-t-1">
「{{ post.category.name }}」の最新5件<br>
{% for latest in post.category.get_latest_post %}
<a href="{% url 'torina_blog:detail' latest.pk %}">{{ latest.title }}</a><br>
{% endfor %}
</div>

<!-- 各タグの最新5件 -->
{% for tag in post.tag.all %}
<div class="col-xs-12 col-sm-6 m-t-1">
「{{ tag.name }}」の最新5件<br>
{% for latest in tag.get_latest_post %}
<a href="{% url 'torina_blog:detail' latest.pk %}">{{ latest.title }}</a><br>
{% endfor %}
</div>
{% endfor %}
</div>


コメントとかパンくずリストとかもつけてくぞー
Django初心者です 約280日前 2018年3月6日22:41 返信する
お世話になっております。
いつもnaritoブログ様でPythonの勉強させていただいております。
ありがとうございます。

ソースコードをダウンロードさせていただきブログ活用させていただいているのですが、sitemapについて一点ご教授いただきたくコメントさせていただいております。

sitemap.xmlを見てみると
<loc>http://example.com/blog/detail/1/</loc>
のようにドメインが「http://example.com/」のようになってしまい、実際のドメインを取得することができずに苦戦しております。
setting.pyでALLOWED_HOSTSの設定もしており、どこが悪いのか検討がつかない状態です。
大変恐れ入りますが、何とぞご教授のほどよろしくお願い申し上げます。
なりと 約280日前 2018年3月6日23:10
admin管理サイトへ移動すると、下側に「サイト」というモデルがあります。
クリックしますと、「example.com」というドメイン名で既に登録されたものがありますので、そちらを書き換えてみてください。
Githubのリードミーなどにも追記しておきます、ありがとうございました。
Django初心者です 約280日前 2018年3月6日23:36 返信する
早速ご回答いただきありがとうございます。
ご教授いただいた方法で変更できました!

新設にご回答いただき大変助かりました。
今後も勉強させていただきます。

何とぞよろしくお願い申し上げます。
名無し 約212日前 2018年5月14日0:04 返信する
いつも参考にさせて頂いております。

一つ伺います。ブログの画像ですが、今はサムネイルをImageFieldで投稿しています。記事の中にも複数の画像を貼って投稿したいのですが、何か方法はあるでしょうか?
なりと 約212日前 2018年5月14日1:05 返信する
本文中に画像を挿入したいということでよろしいですか。

記事の本文をHTMLとして保存しておく方法があります。
他の方法ですと、記事内に[filter img]https....png[end]のような目印をつけておき、それをテンプレートフィルタ等を使って都度<img src="https://..."に置換していく方法です。

後はアップロード画像を表すモデルを定義し、記事とそれをManyToManyで紐付けたり、アップロード画像側のモデルからForeignKey(Post)のようにしておけば、複数の画像を記事と紐付けれます。
名無し 約204日前 2018年5月22日1:38
ありがとうございます。早速試してみます。
でんろう 約140日前 2018年7月24日13:53 返信する
下記ページも Djagnoのデータベース操作方法について便利です。
https://opendata-web.site/blog/entry/15/
なりと 約140日前 2018年7月25日0:53
情報ありがとうございます。