Django Fantom Blog-2

Toku
Post Creation Formを作成する
参考:Udemyの Django 3.0 MasterClass - Learn How To Create Django Apps
user@mbp Django-fantom-blog % source venv/bin/activate
(venv) user@mbp Django-fantom-blog % python manage.py runserver
投稿用のフォーム(PostCreationForm)を作成する
# posts/forms.py
from .models import *
from django import forms
class PostCreationForm(forms.ModelForm):
class Meta:
model = Post
widgets = {
'title':forms.TextInput(attrs={'class':'single-input', 'placeholder':'タイトル'}),
'content':forms.Textarea(attrs={'class':'single-input', 'placeholder':'内容'})
}
fields = ['title', 'category', 'content', 'image']
テンプレートファイルにPostCreationFormを表示する
CreatePostViewを作成する
# posts.views.py
class CreatePostView(CreateView):
template_name = 'posts/create-post.html'
form = PostCreationForm
models = Post
テンプレートファイルcreate-post.htmlを作成する
login.htmlをコピーして編集する
# templates/posts/create-post.html
{% extends 'base.html'%}
{% load static %}
{% block content %}
<!--================Home Banner Area =================-->
<section class="banner_area">
<div class="banner_inner d-flex align-items-center">
<div class="overlay bg-parallax" data-stellar-ratio="0.9" data-stellar-vertical-offset="0" data-background=""></div>
<div class="container">
<div class="banner_content text-center">
<h2>投稿作成</h2>
<div class="page_link">
<a href="{% url 'index' %}">Home</a>
</div>
</div>
</div>
</div>
</section>
<!--================End Home Banner Area =================-->
<!--================Blog Area =================-->
<section class="blog_area p_120">
<div class="container">
<div class="row">
<div class="col-lg-8">
<h2 style="text-align: center; color:blue">投稿作成</h2>
<div class="blog_left_sidebar">
<form method='post' enctype="multipart/form-data">
{% csrf_token %}
{% if form.errors %}
<div id="errors">
<div class="inners">
<p style="color:red">以下のエラーがあります</p>
<ul>
{% for field in form %}
{% if field.errors %}
<li>{{form.label }}: {{field.errors|striptags }}</li>
{% endif %}
{% endfor %}
</ul>
</div>
</div>
{% endif %}
{% for field in form %}
<div class="mt-10">
{{ field }}
</div>
{% endfor %}
<div class="mt-10">
<input type="text" name="tag" placeholder="タグ" onfocus="this.placeholder = ''" onblur="this.placeholder = 'タグ'" required class="single-input">
</div>
<input type="submit" class="genric-btn success circle" style="float:right;margin-top:30px;" value="投稿する" >
</form>
</div>
</div>
{% include 'right_side.html' %}
</div>
</div>
</section>
{% endblock %}
urls.pyにpost作成のパスを追加
# posts/urls.py
from django.urls import path
# from posts.views import *
from .views import *
urlpatterns = [
path('', IndexView.as_view(), name="index"),
path('detail/<str:pk>', PostDetail.as_view(), name="detail"),
path('category/<str:pk>', CategoryDetail.as_view(), name="category_detail"),
path('tag/<slug:slug>', TagDetail.as_view(), name="tag_detail"),
path('post-create', CreatePostView.as_view(), name="create_post"), # added
]
作成したPostを保存する
# posts.views.py
from django.shortcuts import get_object_or_404
from .models import Post, Category, Tag
from django.views.generic import ListView, DetailView, CreateView
from .forms import PostCreationForm
from django.utils.decorators import method_decorator
from django.contrib.auth.decorators import login_required
from django.template.defaultfilters import slugify
from django.urls import reverse
class IndexView(ListView):
class PostDetail(DetailView):
class CategoryDetail(ListView):
class TagDetail(ListView):
@method_decorator(login_required(login_url='users/login'), name='dispatch')
class CreatePostView(CreateView):
template_name = 'posts/create-post.html'
form_class = PostCreationForm
models = Post
def get_success_url(self):
return reverse('detail', kwargs={"pk":self.object.id})
def form_valid(self, form):
form.instance.user = self.request.user
form.save()
tags = self.request.POST.get('tag').split(',')
# for tag in tags:
# tag, created = Tag.objects.get_or_create(title=tag)
# form.instance.tags.add(tag)
# return super(CreatePostView, self).form_valid(form)
for tag in tags:
current_tag = Tag.objects.filter(slug=slugify(tag))
if current_tag.count() < 1:
create_tag = Tag.objects.create(title=tag)
create_tag.slug = slugify(create_tag.title)
create_tag.save()
form.instance.tag.add(create_tag)
print('create')
else:
existed_tag = Tag.objects.get(slug=slugify(tag))
form.instance.tag.add(existed_tag)
print('exist')
return super(CreatePostView, self).form_valid(form)
tags = self.request.POST.get('tag').split(',') のところのget('tag')のtagは、 create-post.htmlのinput type="text" name="tag" のnameから持ってきている。またsplit関数はリストを返のでtagsはリストになる
Tagモデルの def save(self, *args, **kwargs): は「Cannot force both insert and updating in model saving」のエラーが出たのでコメントアウトした
uccess_urlとget_success_urlおよびreverseとreverse_lazyの使い分け
https://btj0.com/blog/django/success_url-get_success_url-reverse-reverse_lazy/
Crispy formsをインストールする
https://django-crispy-forms.readthedocs.io/en/latest/
(venv) user@mbp Django-fantom-blog % pip install django-crispy-forms
# Django_fantom_blog/settings.py
INSTALLED_APPS = (
...
'crispy_forms',
)
CRISPY_TEMPLATE_PACK = 'bootstrap4'
https://django-crispy-forms.readthedocs.io/en/latest/filters.html
以下のように使う
{% load crispy_forms_tags %}
<form method="post" class="my-class">
{{ my_formset|crispy }}
</form>
Post Update Formを作成する
FormHelperを使う
https://django-crispy-forms.readthedocs.io/en/latest/form_helper.html
from crispy_forms.helper import FormHelper
class ExampleForm(forms.Form):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper(self)
# post/forms.py
from .models import *
from django import forms
from crispy_forms.helper import FormHelper, Field, Submit
from crispy_forms.layout import Layout,
class PostCreationForm(forms.ModelForm):
・・・
class PostUpdateForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(PostUpdateForm, self).__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_method='post'
self.helper.field_class='mt-10'
self.helper.layout = Layout(
Field("title", css_class="single-input", placeholder="Title"),
Field("category", css_class="single-input"),
Field("content", css_class="single-input", placeholder="Content"),
Field("image", css_class="single-input"),
Field("tag", css_class="single-input", placeholder="Your Tags", value=self.instance.post_tag())
)
self.helper.add_input(Submit('submit', '更新', css_class="generic-btn success circle"))
tag = forms.CharField() # あとで追加
class Meta:
model=Post
fields = ['title', 'category', 'content', 'image']
Post Update Template Fileを作成する
最初にUpdatePostViewを作成
# posts/views.py
from .models import Post, Category, Tag
from django.views.generic import ListView, DetailView, CreateView, UpdateView
from .forms import PostCreationForm, PostUpdateForm
class UpdatePostView(UpdateView):
model = Post
template_name="posts/post-update.html"
form_class=PostUpdateForm
from django.urls import reverse
def get_success_url(self):
return reverse('detail', kwargs={"pk":self.object.id})
次にurls.pyにパスを追加
# posts/urls.py
from django.urls import path
# from posts.views import *
from .views import *
urlpatterns = [
path('', IndexView.as_view(), name="index"),
path('detail/<str:pk>', PostDetail.as_view(), name="detail"),
path('post-update/<str:pk>', UpdatePostView.as_view(), name="post_update"), # added
path('category/<str:pk>', CategoryDetail.as_view(), name="category_detail"),
path('tag/<slug:slug>', TagDetail.as_view(), name="tag_detail"),
path('post-create', CreatePostView.as_view(), name="create_post"),
]
テンプレートファイルを作成する
{% load crispy_forms_tags %}や {% crispy form %}を以下のように挿入する
# templates/posts/post-update.html
{% extends 'base.html'%}
{% load static %}
{% load crispy_forms_tags %} # added
{% block content %}
<!--================Home Banner Area =================-->
<section class="banner_area">
<div class="banner_inner d-flex align-items-center">
<div class="overlay bg-parallax" data-stellar-ratio="0.9" data-stellar-vertical-offset="0" data-background=""></div>
<div class="container">
<div class="banner_content text-center">
<h2>投稿作成</h2>
<div class="page_link">
<a href="{% url 'index' %}">Home</a>
</div>
</div>
</div>
</div>
</section>
<!--================End Home Banner Area =================-->
<!--================Blog Area =================-->
<section class="blog_area p_120">
<div class="container">
<div class="row">
<div class="col-lg-8">
<h2 style="text-align: center; color:blue">投稿作成</h2>
<div class="blog_left_sidebar">
{% crispy form %} # added
</div>
</div>
{% include 'right_side.html' %}
</div>
</div>
</section>
{% endblock %}
http://localhost:8000/post-update/1eaf4204-3c91-4a65-b988-a7712dba91b8 を入力すると編集画面が表示される
タグを編集画面に表示させるため,forms.pyのPostUpdateFormにtag = forms.CharField()を追加し、また以下のようにPostモデルにpost_tagを追加する
またforms.pyのPostUpdateFormクラスあるtagのFieldにvalue=self.instance.post_tag()を追加する
Field("tag", css_class="single-input", placeholder="Your Tags",value=self.instance.post_tag())
# posts/models.py
class Post(models.Model):
title = models.CharField(verbose_name='タイトル',max_length=150)
content = models.TextField(verbose_name='内容')
publishing_date=models.DateField(verbose_name='投稿日', auto_now_add=True)
image = models.ImageField(verbose_name='画像',null=True, blank=True, upload_to='uploads/')
user = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name='ユーザ',on_delete=models.CASCADE)
id = models.UUIDField(default=uuid.uuid4, unique=True, primary_key=True, editable=False)
# slug = models.SlugField(default="slug")
category = models.ForeignKey(Category, on_delete=models.CASCADE, null=True, blank=True,verbose_name='カテゴリー', related_name='posts')
tag = models.ManyToManyField(Tag, related_name="posts" , blank=True, verbose_name='タグ')
slider_post = models.BooleanField(default=False, verbose_name='スライダー')
def __str__(self):
return self.title
def post_tag(self): # added
return ','.join(str(tag) for tag in self.tag.all())
Postを更新する
他のログインした人がpost-updateのURLを叩いても編集できないように以下のdef get(self, request, *args, **kwargs)のようなチェック機能を追加する
# post/views.py
from django.http import HttpResponseRedirect
・・・
@method_decorator(login_required(login_url='users/login'), name='dispatch')
class UpdatePostView(UpdateView):
model = Post
template_name="posts/post-update.html"
form_class=PostUpdateForm
def get_success_url(self):
return reverse('detail', kwargs={"pk":self.object.id})
def form_valid(self, form):
form.instance.user = self.request.user
form.instance.tag.clear()
tags = self.request.POST.get('tag').split(',')
for tag in tags:
current_tag = Tag.objects.filter(slug=slugify(tag))
if current_tag.count() < 1:
newTag = Tag.objects.create(title=tag)
newTag.slug = slugify(newTag.title)
newTag.save()
form.instance.tag.add(newTag)
print('create')
else:
existed_tag = Tag.objects.get(slug=slugify(tag))
form.instance.tag.add(existed_tag)
print('exist')
return super(UpdatePostView, self).form_valid(form)
def get(self, request, *args, **kwargs):
self.object = self.get_object()
if self.object.user != request.user:
return HttpResponseRedirect('/')
return super(UpdatePostView, self).get(request, *args, **kwargs)
削除とアップデートボタンを作成する
投稿の詳細画面に更新と削除ボタンを作成する
class PostDetail(DetailView):
context_object_name = 'single'
`# templates/posts/detail.html
<p>{{single.content}}</p>
{% if single.user == request.user %} # ログインした人が作成したPostのみ表示できるようする
<a href="{% url 'post_update' single.pk %}" class="genric-btn info circle">更新</a>
<a href="{% url 'post_update' single.pk %}" class="genric-btn danger circle">削除</a>
{% endif %}
Postを削除する
# posts/views.py
class DeletePostView(DeleteView):
model = Post
success_url = '/'
template_name = 'posts/delete.html'
def delete(self, request, *args, **kwargs):
self.object = self.get_object()
if self.object.user == request.user:
self.object.delete()
return HttpResponseRedirect(self.success_url) # homeに戻る
else:
return HttpResponseRedirect(self.success_url)
# return HttpResponseRedirect()
# 投稿してない人が削除画面を見れないようにする
def get(self, request, *args, **kwargs):
self.object = self.get_object()
if self.object.user != request.user:
return HttpResponseRedirect('/')
return super(DeletePostView, self).get(request, *args, **kwargs)
# posts/urls.py
from django.urls import path
from .views import *
urlpatterns = [
path('', IndexView.as_view(), name="index"),
path('detail/<str:pk>', PostDetail.as_view(), name="detail"),
path('post-update/<str:pk>', UpdatePostView.as_view(), name="post_update"),
path('post-delete/<str:pk>', DeletePostView.as_view(), name="post_delete"), # added
path('category/<str:pk>', CategoryDetail.as_view(), name="category_detail"),
path('tag/<slug:slug>', TagDetail.as_view(), name="tag_detail"),
path('post-create', CreatePostView.as_view(), name="create_post"),
]
`# templates/posts/detail.html
<p>{{single.content}}</p>
{% if single.user == request.user %}
<a href="{% url 'post_update' single.pk %}" class="genric-btn info circle">更新</a>
<a href="{% url 'post_delete' single.pk %}" class="genric-btn danger circle">削除</a> # changed
{% endif %}
削除用テンプレートhtmlを作成する(post-update.htmlをコピー)
下の a href="{% url 'detail' object.pk %}" はDeletePostViewでpost.pkを使えないからobject.pkを使う
`# templates/posts/delete.html
{% extends 'base.html'%}
{% load static %}
{% block content %}
<!--================Home Banner Area =================-->
<section class="banner_area">
<div class="banner_inner d-flex align-items-center">
<div class="overlay bg-parallax" data-stellar-ratio="0.9" data-stellar-vertical-offset="0" data-background=""></div>
<div class="container">
<div class="banner_content text-center">
<h2>投稿削除</h2>
<div class="page_link">
<a href="{% url 'index' %}">Home</a>
</div>
</div>
</div>
</div>
</section>
<!--================End Home Banner Area =================-->
<!--================Blog Area =================-->
<section class="blog_area p_120">
<div class="container">
<div class="row">
<div class="col-lg-8">
<h2 style="text-align: center; color:blue">投稿削除</h2>
<div class="blog_left_sidebar">
<form method="post">
{% csrf_token %}
<a href="{% url 'detail' object.pk %}" class="genric-btn info circle">No</a>
<button type="submit" class="genric-btn danger circle">Yes</button>
</form>
</div>
</div>
{% include 'right_side.html' %}
</div>
</div>
</section>
{% endblock %}
Navbarを編集する
<!--================Header Menu Area =================-->
<header class="header_area">
<div class="logo_part">
<div class="container">
<a class="logo" href="#"><img src="{% static "img/logo.png" %}" alt=""></a>
</div>
</div>
<div class="main_menu">
<nav class="navbar navbar-expand-lg navbar-light">
<div class="container">
<!-- Brand and toggle get grouped for better mobile display -->
<a class="navbar-brand logo_h" href="{% url 'index' %}"><img src="{% static "img/logo.png" %}" alt=""></a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<!-- Collect the nav links, forms, and other content for toggling -->
<div class="collapse navbar-collapse offset" id="navbarSupportedContent">
<ul class="nav navbar-nav menu_nav">
<li class="nav-item active"><a class="nav-link" href="{% url 'index' %}">ホーム</a></li>
<li class="nav-item"><a class="nav-link" href="category.html">カテゴリー</a></li>
<li class="nav-item"><a class="nav-link" href="archive.html">Archive</a></li>
{% if user.is_authenticated %}
<li class="nav-item submenu dropdown">
<a href="#" class="nav-link dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">{{user.username}}</a>
<ul class="dropdown-menu">
<li class="nav-item"><a class="nav-link" href="{% url 'create_post' %}">投稿の追加</a></li>
<li class="nav-item"><a class="nav-link" href="elements.html">パスワードの変更</a></li>
<li class="nav-item"><a class="nav-link" href="elements.html">プロフィール</a></li>
<li class="nav-item"><a class="nav-link" href="{% url 'users:logout' %}">ログアウト</a></li>
</ul>
</li>
{% else %}
<li class="nav-item"><a class="nav-link" href="{% url 'users:register' %}">登録</a></li>
<li class="nav-item"><a class="nav-link" href="{% url 'users:login' %}">ログイン</a></li>
{% endif %}
<li class="nav-item"><a class="nav-link" href="contact.html">お問合せ</a></li>
</ul>
<ul class="nav navbar-nav navbar-right ml-auto">
<li class="nav-item"><a href="#" class="search"><i class="lnr lnr-magnifier"></i></a></li>
</ul>
</div>
</div>
</nav>
</div>
</header>
<!--================Header Menu Area =================-->
{% block content %}
人気順投稿リストを作成する
# Posts/models.py
class Post(models.Model):
title = models.CharField(verbose_name='タイトル',max_length=150)
content = models.TextField(verbose_name='内容')
publishing_date=models.DateField(verbose_name='投稿日', auto_now_add=True)
image = models.ImageField(verbose_name='画像',null=True, blank=True, upload_to='uploads/')
user = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name='ユーザ',on_delete=models.CASCADE)
id = models.UUIDField(default=uuid.uuid4, unique=True, primary_key=True, editable=False)
# slug = models.SlugField(default="slug")
category = models.ForeignKey(Category, on_delete=models.CASCADE, null=True, blank=True,verbose_name='カテゴリー', related_name='posts')
tag = models.ManyToManyField(Tag, related_name="posts" , blank=True, verbose_name='タグ')
slider_post = models.BooleanField(default=False, verbose_name='スライダー')
hit = models.PositiveIntegerField(default=0) # added
def __str__(self):
return self.title
def post_tag(self):
return ','.join(str(tag) for tag in self.tag.all())
makemirations & migrateを実行
F関数を使ってDjangoで効率よくquerysetのカウントアップを行う
https://tksmml.hatenablog.com/entry/2019/08/05/170000
# posts/views.py
from django.db.models import F # added F関数
class PostDetail(DetailView):
template_name="posts/detail.html"
model = Post
context_object_name = 'single'
def get(self, request, *args, **kwargs): # added
self.hit = Post.objects.filter(id=self.kwargs['pk'].update(hit=F('hit')+1))
return super(PostDetail,self).get(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
return context
# posts/templatetags/custom_tags.py
from django import template
from posts.models import Category, Tag, Post
register = template.Library()
@register.simple_tag(name="post_categories")
def all_categories():
return Category.objects.all()
@register.simple_tag(name="tags")
def all_tags():
return Tag.objects.all()
@register.simple_tag(name="hit_posts") # added
def hit_posts():
return Post.objects.order_by('-hit')[:5]
右サイドのhtmlを編集する
<!-- right_side.html -->
<aside class="single_sidebar_widget popular_post_widget">
<h3 class="widget_title">人気 投稿</h3>
{% hit_posts as posts %}
{% for post in posts %}
<div class="media post_item">
<img src="{{ post.image.url }}" alt="post" style="width: 100px; height:60px">
<div class="media-body">
<a href="{% url 'detail' post.id %}"><h3>{{ post.title }}</h3></a>
<p>閲覧回数: {{ post.hit }}</p>
</div>
</div>
{% endfor %}
<div class="br"></div>
</aside>
テンプレートのタグを表示する
詳細画面のタグを表示する
singleはviews.pyのPostDetail(DetailView)で定義しているcontext_object_name
<!-- templates/posts/detail.html -->
<a href="#"><h4>{{single.title}}</h4></a>
<div class="user_details">
<div class="float-left">
{% for tag in single.tag.all %}
<a href="{% url 'tag_detail' tag.slug %}">{{ tag.title }}</a>
{% endfor %}
</div>
homeページのタグを表示する
下記のpostは {% for post in posts %} のpost
<!-- templates/posts/index.html -->
<div class="blog_text_inner">
{% for tag in post.tag.all %} // added
<a class="cat" href="#">{{ tag.title }}</a> // changed
{% endfor %} // added
<a href="{% url 'detail' pk=post.id %}"><h4>{{ post.title }}</h4></a>
<p>{{ post.content|truncatechars:175 }}</p>
<div class="date">
<a href="#"><i class="fa fa-calendar" aria-hidden="true"></i>{{ post.publishing_date }}</a>
<a href="#"><i class="fa fa-comments-o" aria-hidden="true"></i> 05</a>
</div>
</div>
カテゴリー別ページとタグ別ページのタグを表示する
# templates/categories/category_detail.html
# <a class="cat" href="#">Gadgets</a> # 削除
{% for tag in post.tag.all %} # added
<a class="cat" href="#">{{ tag.title }}</a>
{% endfor %}
# templates/tags/tag_detail.html
# <a class="cat" href="#">Gadgets</a> # 削除
{% for tag in post.tag.all %} # added
<a class="cat" href="#">{{ tag.title }}</a>
{% endfor %}
前の投稿と次の投稿を表示する
詳細ページから前の投稿と次の投稿に遷移できるようにする
現在表示されているPostのidはself.kwargs['pk']で表すことができる
ltはless thanを意味する
# post/views.py
class PostDetail(DetailView):
template_name="posts/detail.html"
model = Post
context_object_name = 'single'
def get(self, request, *args, **kwargs):
self.hit = Post.objects.filter(id=self.kwargs['pk']).update(hit=F('hit')+1)
return super(PostDetail,self).get(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
print('context',context['object'].publishing_date)
pubdate = context['object'].publishing_date
# context['Next'] = Post.objects.filter(id__gt=self.kwargs['pk']).order_by('ーpk').first()
context['previous'] = Post.objects.filter(publishing_date__lt=pubdate).order_by('-publishing_date').first()
print('previous',context['previous'])
# context['next'] = Post.objects.filter(id__gt=self.kwargs['pk']).order_by('pk').first()
context['next'] = Post.objects.filter(publishing_date__gt=pubdate).order_by('publishing_date').first()
print('next',context['next'])
return context
details.htmlのnavigation-areaを編集する
# templates/tags/tag_detail.html
<div class="navigation-area">
<div class="row">
{% if previous %}
<div class="col-lg-6 col-md-6 col-12 nav-left flex-row d-flex justify-content-start align-items-center">
<div class="thumb" >
<a href="{% url 'detail' previous.pk %}"><img class="img-fluid" src="{{ previous.image.url }}" alt="" style="width:100px;hight:100px">></a>
</div>
<div class="arrow">
<a href="{% url 'detail' previous.pk %}"><span class="lnr text-white lnr-arrow-left"></span></a>
</div>
<div class="detials">
<p>前の記事</p>
<a href="{% url 'detail' previous.pk %}"><h4>{{ previous.title }}</h4></a>
</div>
</div>
{% else %}
<div class="col-lg-6 col-md-6 col-12 nav-left flex-row d-flex justify-content-start align-items-center">
<div class="thumb">
<a href="#"><img class="img-fluid" src="{% static "img/blog/prev.jpg" %}" alt="" style="width:100px;hight:100px"></a>
</div>
<div class="arrow">
<a href="#"><span class="lnr text-white lnr-arrow-left"></span></a>
</div>
<div class="detials">
<p>前の記事なし</p>
<a href="#"><h4>前の記事なし</h4></a>
</div>
</div>
{% endif %}
{% if next %}
<div class="col-lg-6 col-md-6 col-12 nav-right flex-row d-flex justify-content-end align-items-center">
<div class="detials">
<p>次の記事</p>
<a href="{% url 'detail' next.pk %}"><h4>{{ next.title }}</h4></a>
</div>
<div class="arrow">
<a href="{% url 'detail' next.pk %}"><span class="lnr text-white lnr-arrow-right"></span></a>
</div>
<div class="thumb">
<a href="{% url 'detail' next.pk %}"><img class="img-fluid" src="{{ next.image.url }}" alt="" style="width:60px;hight:60px"></a>
</div>
</div>
{% else %}
<div class="col-lg-6 col-md-6 col-12 nav-right flex-row d-flex justify-content-end align-items-center">
<div class="detials">
<p>次の記事無</p>
<a href="#"><h4>No Next Post</h4></a>
</div>
<div class="arrow">
<a href="#"><span class="lnr text-white lnr-arrow-right"></span></a>
</div>
<div class="thumb">
<a href="#"><img class="img-fluid" src="{% static "img/blog/next.jpg" %}" alt="" style="width:60px;hight:60px"></a>
</div>
</div>
{% endif %}
</div>
</div>
ページネーションを作成する
index.htmlを編集する
<!-- templates/posts/index.html -->
{% if is_paginated %}
<nav class="blog-pagination justify-content-center d-flex">
<ul class="pagination">
{% if page_obj.has_previous %}
<li class="page-item">
<a href="?page={{ page_obj.has_previous_page_number }}" class="page-link" aria-label="Previous">
<span aria-hidden="true">
<span class="lnr lnr-chevron-left"></span>
</span>
</a>
</li>
{% else %}
<li class="page-item disabled">
<a href="#" class="page-link" aria-label="Previous">
<span aria-hidden="true">
<span class="lnr lnr-chevron-left"></span>
</span>
</a>
</li>
{% endif %}
{% for i in paginator.page_range %}
{% if page_obj.number == i %}
<li class="page-item active"><a href="#" class="page-link">{{ i }}</a></li>
{% else %}
<li class="page-item"><a href="?page={{ i }}" class="page-link">{{ i }}</a></li>
{% endif %}
{% endfor %}
{% if page_obj.has_next %}
<li class="page-item">
<a href="?page={{ page_obj.has_next_page_number }}" class="page-link" aria-label="Next">
<span aria-hidden="true">
<span class="lnr lnr-chevron-right"></span>
</span>
</a>
</li>
{% else %}
<li class="page-item disabled">
<a href="#" class="page-link" aria-label="Next">
<span aria-hidden="true">
<span class="lnr lnr-chevron-right"></span>
</span>
</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
post/views.pyにpaginate_by = 3 を追加する
class IndexView(ListView):
template_name="posts/index.html"
model = Post
context_object_name = 'posts'
paginate_by = 3 # added
・・・
class CategoryDetail(ListView):
model = Post
template_name="categories/category_detail.html"
context_object_name="posts"
paginate_by = 3 # added
・・・
class TagDetail(ListView):
model = Post
template_name="tags/tag_detail.html"
context_object_name="posts"
paginate_by = 3 # added
パスワード変更Htmlを作成する
まずパスワード変更用のパスを登録する
# users.urls.view
from django.urls import path
from .views import *
from django.contrib.auth import views as authViews # added
app_name="users"
urlpatterns = [
path('register/', RegisterView.as_view(), name="register"),
path('login/', UserLoginView.as_view(), name="login"),
path('logout/', UserLogoutView.as_view(), name="logout"),
path('password-change/', authViews.PasswordChangeView.as_view(success_url=reverse_lazy('users:password_change_done')), name="password_change"),
path('password-change-done/', authViews.PasswordChangeDoneView.as_view(), name="password_change_done")
]
ユーザパスワードの変更
createPostViewの@method_decorator(login_required(login_url='users/login'), name='dispatch')を参照する
users/urls.pyのPasswordChangeViewを右クリックして定義へ移動する
class PasswordChangeView(PasswordContextMixin, FormView):
form_class = PasswordChangeForm
success_url = reverse_lazy('users:password_change_done') # changed
template_name = "registration/password_change_form.html"
title = _("Password change")
@method_decorator(sensitive_post_parameters())
@method_decorator(csrf_protect)
@method_decorator(login_required(login_url='users/login')) # changed
def dispatch(self, *args, **kwargs):
return super().dispatch(*args, **kwargs)
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs["user"] = self.request.user
return kwargs
def form_valid(self, form):
form.save()
# Updating the password logs out all other sessions for the user
# except the current one.
update_session_auth_hash(self.request, form.user)
return super().form_valid(form)
class PasswordChangeDoneView(PasswordContextMixin, TemplateView):
template_name = "registration/password_change_done.html"
title = _("Password change successful")
@method_decorator(login_required)
def dispatch(self, *args, **kwargs):
return super().dispatch(*args, **kwargs)
パスワード変更フォーム用とパスワード変更完了通知用のhtmlを作成する
templatesフォルダの直下にregistrationフォルダを作りその直下に2ファイル作成する
<!-- templates/registration/password_change_form.html -->
{% extends 'base.html'%}
{% load static %}
{% block content %}
<!--================Home Banner Area =================-->
<section class="banner_area">
<div class="banner_inner d-flex align-items-center">
<div class="overlay bg-parallax" data-stellar-ratio="0.9" data-stellar-vertical-offset="0" data-background=""></div>
<div class="container">
<div class="banner_content text-center">
<h2>登録</h2>
<div class="page_link">
<a href="{% url 'index' %}">Home</a>
</div>
</div>
</div>
</div>
</section>
<!--================End Home Banner Area =================-->
<!--================Blog Area =================-->
<section class="blog_area p_120">
<div class="container">
<div class="row">
<div class="col-lg-8">
<h2 style="text-align: center; color:blue">パスワード変更</h2>
<div class="blog_left_sidebar">
<form method='post'>
{% csrf_token %}
{% if form.errors %}
<div id="errors">
<div class="inners">
<p style="color:red">以下のエラーがあります</p>
<ul>
{% for field in form %}
{% if field.errors %}
<li>{{form.label }}: {{field.errors|striptags }}</li>
{% endif %}
{% endfor %}
</ul>
</div>
</div>
{% endif %}
{# {{ form.as_p }} #}
<div class="mt-10">
<input type="password" name="old_password" placeholder="古いパスワード" onfocus="this.placeholder = ''" onblur="this.placeholder = '古いパスワード'" required class="single-input">
</div>
<div class="mt-10">
<input type="password" name="new_password1" placeholder="新しいパスワード" onfocus="this.placeholder = ''" onblur="this.placeholder = '新しいパスワード'" required class="single-input">
</div>
<div class="mt-10">
<input type="password" name="new_password2" placeholder="新しいパスワード(確認)" onfocus="this.placeholder = ''" onblur="this.placeholder = '新しいパスワード(確認)'" required class="single-input">
</div>
<input type="submit" class="genric-btn success circle" style="float:right;margin-top:30px;" value="変更" >
</form>
</div>
</div>
{% include 'right_side.html' %}
</div>
</div>
</section>
{% endblock %}
<!-- templates/registration/password_change_done.html -->
{% extends 'base.html'%}
{% load static %}
{% block content %}
<!--================Home Banner Area =================-->
<section class="banner_area">
<div class="banner_inner d-flex align-items-center">
<div class="overlay bg-parallax" data-stellar-ratio="0.9" data-stellar-vertical-offset="0" data-background=""></div>
<div class="container">
<div class="banner_content text-center">
<h2>登録</h2>
<div class="page_link">
<a href="{% url 'index' %}">Home</a>
</div>
</div>
</div>
</div>
</section>
<!--================End Home Banner Area =================-->
<!--================Blog Area =================-->
<section class="blog_area p_120">
<div class="container">
<div class="row">
<div class="col-lg-8">
<h2 style="text-align: center; color:blue">パスワードが変更されました</h2>
<div class="blog_left_sidebar">
</div>
</div>
{% include 'right_side.html' %}
</div>
</div>
</section>
{% endblock %}
検索機能を作成する
検索ボックスにformタグを追加して、action="{% url 'search' %}" method="get"とする
inputタグにname="q"を追加する
<!-- templates/right_side.html -->
{% load static %}
{% load custom_tags %}
<div class="col-lg-4">
<div class="blog_right_sidebar">
<aside class="single_sidebar_widget search_widget">
<form action="{% url 'search' %}" method="get" >
<div class="input-group">
<input type="text" class="form-control" placeholder="Search Posts" name="q">
<span class="input-group-btn">
<button class="btn btn-default" type="button"><i class="lnr lnr-magnifier"></i></button>
</span>
</div><!-- /input-group -->
</form>
<div class="br"></div>
</aside>
SearchView(ListView)を作成する
self.request.GET.get("q")のGETはmethod="get"から来て、get("q")のgetは関数でname="q"を参照している
重複しないように.distinct()をつける
from django.db.models import Q
class SearchView(ListView): # 78
model = Post
template_name = 'posts/search.html'
paginate_by = 5
context_object_name = 'posts'
def get_queryset(self):
query = self.request.GET.get("q")
if query:
return Post.objects.filter(Q(title__icontains=query) |
Q(content__icontains=query) |
Q(tag__title__icontains=query)
).order_by('publishing_date').distinct()
return Post.objects.all().order_by('publishing_date')
category_detail.htmlを使ってsearch.htmlを作成する
# posts/search.html
{% extends 'base.html'%}
{% load static %}
{% block content %}
<!--================Home Banner Area =================-->
<section class="banner_area">
<div class="banner_inner d-flex align-items-center">
<div class="overlay bg-parallax" data-stellar-ratio="0.9" data-stellar-vertical-offset="0" data-background=""></div>
<div class="container">
<div class="banner_content text-center">
<h2>検索結果</h2>
<div class="page_link">
<a href="{% url 'index' %}">Home</a>
<a href="#"></a>
</div>
</div>
</div>
</div>
</section>
<!--================End Home Banner Area =================-->
<!--================Blog Area =================-->
<section class="blog_area p_120">
<div class="container">
<div class="row">
<div class="col-lg-8">
<div class="blog_left_sidebar">
{% for post in posts %}
<article class="blog_style1">
<div class="blog_img">
<img class="img-fluid" src={{ post.image.url }} alt="">
</div>
<div class="blog_text">
<div class="blog_text_inner">
{% for tag in post.tag.all %}
<a class="cat" href="#">{{ tag.title }}</a>
{% endfor %}
<a href="{% url 'detail' pk=post.id %}"><h4>{{ post.title | truncatechars:150}}</h4></a>
<p>{{ post.content }}</p>
<div class="date">
<a href="#"><i class="fa fa-calendar" aria-hidden="true"></i>{{post.publishing_date}}</a>
<a href="#"><i class="fa fa-comments-o" aria-hidden="true"></i> 05</a>
</div>
</div>
</div>
</article>
{% endfor %}
<!--==パジネーションエリア ↓==-->
{% if is_paginated %}
<nav class="blog-pagination justify-content-center d-flex">
<ul class="pagination">
{% if page_obj.has_previous %}
<li class="page-item">
<a href="?q={{request.GET.get.q}}&page={{ page_obj.has_previous_page_number }}" class="page-link" aria-label="Previous">
<span aria-hidden="true">
<span class="lnr lnr-chevron-left"></span>
</span>
</a>
</li>
{% else %}
<li class="page-item disabled">
<a href="#" class="page-link" aria-label="Previous">
<span aria-hidden="true">
<span class="lnr lnr-chevron-left"></span>
</span>
</a>
</li>
{% endif %}
{% for i in paginator.page_range %}
{% if page_obj.number == i %}
<li class="page-item active"><a href="#" class="page-link">{{ i }}</a></li>
{% else %}
<li class="page-item"><a href="?q={{request.GET.get.q}}&page={{ i }}" class="page-link">{{ i }}</a></li>
{% endif %}
{% endfor %}
{% if page_obj.has_next %}
<li class="page-item">
<a href="?q={{request.GET.get.q}}&page={{ page_obj.has_next_page_number }}" class="page-link" aria-label="Next">
<span aria-hidden="true">
<span class="lnr lnr-chevron-right"></span>
</span>
</a>
</li>
{% else %}
<li class="page-item disabled">
<a href="#" class="page-link" aria-label="Next">
<span aria-hidden="true">
<span class="lnr lnr-chevron-right"></span>
</span>
</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
</div>
</div>
{% include 'right_side.html' %}
</div>
</div>
</section>
<!--================Blog Area =================-->
{% endblock %}
posts/urls.pyのpathにsearchを追加する
# posts/urls.py
from .views import *
urlpatterns = [
path('', IndexView.as_view(), name="index"),
path('detail/<str:pk>', PostDetail.as_view(), name="detail"),
path('post-update/<str:pk>', UpdatePostView.as_view(), name="post_update"),
path('post-delete/<str:pk>', DeletePostView.as_view(), name="post_delete"),
path('category/<str:pk>', CategoryDetail.as_view(), name="category_detail"),
path('tag/<slug:slug>', TagDetail.as_view(), name="tag_detail"),
path('post-create', CreatePostView.as_view(), name="create_post"),
path('search', SearchView.as_view(), name="search"), # added
]
検索ボタンをクリックすると以下のURLが表示される
http://127.0.0.1:8000/search?q=lorem
またペジネーションボタンを押すとhttp://127.0.0.1:8000/search?page=2 になり、q=loremが消えているのでhtmlを以下のように修正する(全部で3ヶ所)
href="?q={{request.GET.get.q}}&page={{ page_obj.has_previous_page_number }}"
これによりURLがhttp://127.0.0.1:8000/search?q=&page=2のように表示される
{% if is_paginated %}
<nav class="blog-pagination justify-content-center d-flex">
<ul class="pagination">
{% if page_obj.has_previous %}
<li class="page-item">
<a href="?q={{request.GET.get.q}}&page={{ page_obj.has_previous_page_number }}" #class="page-link" aria-label="Previous"> // changed
<span aria-hidden="true">
<span class="lnr lnr-chevron-left"></span>
</span>
</a>
</li>
{% else %}
コメント機能を作成する
コメントのモデルを作成する
def str(self)で、self.post.titleを返した方がコメントの管理がやりやすい
# posts/models.py
class Comment(models.Model):
post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name="comments", null=True, blank=True, verbose_name='Post')
name = models.CharField(max_length=100,verbose_name='名前')
email = models.EmailField(max_length=100, verbose_name='メールアドレス')
content = models.TextField(verbose_name='内容')
publishing_date = models.DateTimeField(auto_now=True, verbose_name='投稿日時')
def __str__(self):
return self.post.title
makemigrations & migrate
管理画面に登録するため、admin.pyでAdminCommentを作成する
# posts/admin.py
from django.contrib import admin
from .models import Post, Category, Tag, Comment
class AdminPost(admin.ModelAdmin):
list_filter = ['publishing_date']
list_display = ['title', 'publishing_date']
search_fields = ['title', 'content']
class Meta:
model = Post
class AdminComment(admin.ModelAdmin): # added
list_filter = ('publishing_date', ) # タプルを使ってもOK
search_fields = ('name', 'email', 'content', 'post__title') # タプルを使ってもOK
class Meta:
model = Comment
admin.site.register(Post, AdminPost)
admin.site.register(Category)
admin.site.register(Tag)
admin.site.register(Comment, AdminComment) # added
コメントのフォームを作成する
# posts/forms.py
class CreateCommentForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(CreateCommentForm, self).__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_method="post"
self.helper.layout = Layout (
Field("name", css_class="form-control", placeholder="名前"),
Field("email", css_class="form-control", placeholder="メールアドレス"),
Field("content", css_class="form-control mb-10", placeholder="内容"),
)
self.helper.add_input(Submit('submit', 'コメントする', css_class="primary-btn submit_btn"))
class Meta:
model = Comment
fields = ['name', 'email', 'content']
views.pyのクラスDetailViewやListViewではFormを使うことができない
CreateViewではFormを使うことができる
PostDetail(DetailView)でFormを使うためにFormMixinを追加する
FormMixin: https://en-junior.com/formview/#form-mixin
# posts/views.py
from django.views.generic.edit import FormMixin # added
from .forms import PostCreationForm, PostUpdateForm, CreateCommentForm # added
class PostDetail(DetailView, FormMixin): # added
template_name="posts/detail.html"
model = Post
context_object_name = 'single'
form_class = CreateCommentForm # added
def get(self, request, *args, **kwargs):
self.hit = Post.objects.filter(id=self.kwargs['pk']).update(hit=F('hit')+1)
return super(PostDetail,self).get(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
print('context',context['object'].publishing_date)
pubdate = context['object'].publishing_date
context['previous'] = Post.objects.filter(publishing_date__lt=pubdate).order_by('-publishing_date').first()
print('previous',context['previous'])
context['next'] = Post.objects.filter(publishing_date__gt=pubdate).order_by('publishing_date').first()
print('next',context['next'])
context['form'] = self.get_form # added
return context
def form_valid(self, form):
form.instance.post = self.object
form.save()
return super().form_valid(form)
# return super(PostDetail, self).form_valid(form)
def post(self, *args, **kwargs):
self.object = self.get_object()
form = self.get_form()
if form.is_valid():
return self.form_valid(form)
else:
return self.form_valid(form)
def get_success_url(self):
return reverse('detail', kwargs={"pk":self.object.id})
form_valid()は、postされた際、validationがOKだった場合にシステムからコールされる関数
# templates/posts/detail.html
{% extends 'base.html'%}
{% load static %}
{% load crispy_forms_tags %} # added
{% block content %}
・・・
<div class="comments-area">
<h4>{{ single.comment_count}} コメント</h4>
{% for comment in single.comments.all %}
<div class="comment-list">
<div class="single-comment justify-content-between d-flex">
<div class="user justify-content-between d-flex">
<div class="thumb">
<img src="{% static "img/blog/c1.jpg" %}" alt="">
</div>
<div class="desc">
<h5><a href="#">{{ comment.name }}</a></h5>
<p class="date">{{ comment.publishing_date }}<</p>
<p class="comment">
{{ comment.content }}
</p>
</div>
</div>
<div class="reply-btn">
<a href="" class="btn-reply text-uppercase">reply</a>
</div>
</div>
</div>
{% endfor %}
</div>
<div class="comment-form">
<h4>Leave a Reply</h4>
{% crispy form %}
</div>
</div>
{% include 'right_side.html' %}
</div>
</div>
</section>
コメントの回数を取得する
Postモデルにコメントカウント用の関数def comment_countを作成する
# posts/model.py
class Post(models.Model):
title = models.CharField(verbose_name='タイトル',max_length=150)
content = models.TextField(verbose_name='内容')
publishing_date=models.DateTimeField(verbose_name='投稿日', auto_now_add=True)
image = models.ImageField(verbose_name='画像',null=True, blank=True, upload_to='uploads/')
user = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name='ユーザ',on_delete=models.CASCADE)
id = models.UUIDField(default=uuid.uuid4, unique=True, primary_key=True, editable=False)
# slug = models.SlugField(default="slug")
category = models.ForeignKey(Category, on_delete=models.CASCADE, null=True, blank=True,verbose_name='カテゴリー', related_name='posts')
tag = models.ManyToManyField(Tag, related_name="posts" , blank=True, verbose_name='タグ')
slider_post = models.BooleanField(default=False, verbose_name='スライダー')
hit = models.PositiveIntegerField(default=0, verbose_name='閲覧回数')
def comment_count(self): # added
return self.comments.all().count()
def __str__(self):
return self.title
def post_tag(self):
return ','.join(str(tag) for tag in self.tag.all())
Recaptcha Packageのインストール
https://pypi.org/project/django-recaptcha/#installation
(venv) user@mbp Django-fantom-blog % pip install django-recaptcha
# Django_fantom_blog/settings.py
INSTALLED_APPS = [
...,
'captcha',
...
]
...
RECAPTCHA_PUBLIC_KEY = 'MyRecaptchaKey123'
RECAPTCHA_PRIVATE_KEY = 'MyRecaptchaPrivateKey456'
公開キーとプライベートキーは以下から取得する
https://www.google.com/recaptcha/admin/create
# posts/form.py
from .models import *
from django import forms
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Layout, Field, Submit
from captcha.fields import ReCaptchaField
...
class CreateCommentForm(forms.ModelForm):
captcha = ReCaptchaField() # added
def __init__(self, *args, **kwargs):
super(CreateCommentForm, self).__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_method="post"
self.helper.layout = Layout (
Field("name", css_class="form-control", placeholder="名前"),
Field("email", css_class="form-control", placeholder="メールアドレス"),
Field("content", css_class="form-control mb-10", placeholder="内容"),
Field("captcha"), # added
)
self.helper.add_input(Submit('submit', 'コメントする', css_class="primary-btn submit_btn"))
class Meta:
model = Comment
fields = ['name', 'email', 'content']
フォームのエラー処理
views.pyのPostDetailクラスのdef form_valid(self, form)にif form.is_valid()条件を追加する
# posts/views.py
class PostDetail(DetailView, FormMixin):
template_name="posts/detail.html"
model = Post
context_object_name = 'single'
form_class = CreateCommentForm
def get(self, request, *args, **kwargs):
self.hit = Post.objects.filter(id=self.kwargs['pk']).update(hit=F('hit')+1)
return super(PostDetail,self).get(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
print('context',context['object'].publishing_date)
pubDate = context['object'].publishing_date
context['previous'] = Post.objects.filter(publishing_date__lt=pubDate).order_by('-publishing_date').first()
print('previous',context['previous'])
context['next'] = Post.objects.filter(publishing_date__gt=pubDate).order_by('publishing_date').first()
print('next',context['next'])
context['form'] = self.get_form
return context
def form_valid(self, form):
if form.is_valid(): # added
form.instance.post = self.object
form.save()
return super().form_valid(form)
else: # added
return super(PostDetail, self).form_invalid(form)
def post(self, *args, **kwargs):
self.object = self.get_object()
form = self.get_form()
if form.is_valid():
return self.form_valid(form)
else:
return self.form_invalid(form)
def get_success_url(self):
return reverse('detail', kwargs={"pk":self.object.id})
エラーメッセージを表示するためにBootsrapを使う
https://getbootstrap.jp/docs/5.0/components/alerts/
components=>Alerts=>Dismissing
<!-- posts/detail.html -->
<div class="comment-form">
<h4>返信する</h4>
{% if form.errors %}
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<div id="form_errors">
{% for key, value in form.errors.items %}
<span class="fieldWrapper">
{{ key }} : {{ value }}
</span>
{% endfor %}
</div>
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
{% endif %}
{% crispy form %}
</div>
ユーザープロファイルを作成する
ユーザモデルの作成
# users/models.py
from django.db import models
from django.conf import settings
from django.template.defaultfilters import slugify
import uuid
from PIL import Image
class UserProfile(models.Model):
user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, verbose_name='ユーザー')
birth_day = models.DateField(null=True, blank=True, verbose_name='誕生日')
bio = models.TextField(max_length=1000, blank=True, verbose_name='自己紹介')
image = models.ImageField(blank=True,null=True, default='users/author.png', upload_to='users', verbose_name='画像')
id = models.UUIDField(verbose_name='ID', default=uuid.uuid4, unique=True, primary_key=True, editable=False)
created_at =models.DateTimeField(verbose_name='登録日時', auto_now_add=True)
# slug = models.SlugField(editable=False)
def save(self, *args, **kwargs):
# self.slug = slugify(self.user.username)
super(UserProfile, self).save(*args, **kwargs)
img = Image.open(self.image.path)
if img.height > 200 or img.width > 200:
new_size = (200, 200)
img.thumbnail(new_size)
img.save(self.image.path)
def __str__(self):
return str(self.user.username)
makemigrations & migrate
画像処理を行う(PIL 利用)
https://daeudaeu.com/django-image-processing/
pip install Pillow
admin.pyにモデルを登録する
# users/admin.py
from django.contrib import admin
from .models import UserProfile
admin.site.register(UserProfile)
プロファイルを作成する
UserProfileにSignalを追加する
# users/models.py
from django.db import models
from django.conf import settings
from django.template.defaultfilters import slugify
import uuid
from PIL import Image
from django.db.models.signals import post_save
class UserProfile(models.Model):
user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, verbose_name='ユーザー')
birth_day = models.DateField(null=True, blank=True, verbose_name='誕生日')
bio = models.TextField(max_length=1000, blank=True, verbose_name='自己紹介')
image = models.ImageField(blank=True,null=True, default='users/author.png', upload_to='users', verbose_name='画像')
id = models.UUIDField(verbose_name='ID', default=uuid.uuid4, unique=True, primary_key=True, editable=False)
created_at =models.DateTimeField(verbose_name='登録日時', auto_now_add=True)
# slug = models.SlugField(editable=False)
def save(self, *args, **kwargs):
# self.slug = slugify(self.user.username)
super(UserProfile, self).save(*args, **kwargs)
img = Image.open(self.image.path)
if img.height > 200 or img.width > 200:
new_size = (200, 200)
img.thumbnail(new_size)
img.save(self.image.path)
def __str__(self):
return str(self.user.username)
# return str(self.id)
def create_user_profile(sender, instance, created, **kwargs): # added
if created:
UserProfile.objects.create(user=instance)
post_save.connect(create_user_profile, sender=settings.AUTH_USER_MODEL) # added
# users/forms.py
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth.models import User
from django import forms
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Layout, Field, Submit
from .models import UserProfile
class RegisterForm(UserCreationForm):
username = forms.CharField(max_length=50)
email = forms.EmailField(max_length=50)
password1 = forms.CharField()
password2 = forms.CharField()
class Meta(UserCreationForm):
model = User
fields = ('username', 'email', 'password1', 'password2' )
labels = {
'username': '名前',
'email': 'メールアドレス',
'password1': 'パスワード',
'password2': 'パスワード(確認)',
}
class UsersProfileForm(forms.ModelForm):
def __init__ (self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_method = 'post'
self.helper.field_class='mt-10'
self.helper.layout = Layout (
Field("birth_day", css_class="single-input", placeholder="誕生日"),
Field("bio", css_class="single-input", placeholder="自己紹介"),
Field("image", css_class="single-input", placeholder="画像"),
)
self.helper.add_input(Submit('submit', '更新', css_class="genric-btn success-border medium"))
class Meta:
model = UserProfile
fields = ('birth_day', 'bio', 'image')
widgets = {
'birth_day': forms.DateInput(attrs={'type':'date'})
}
widgetsを'birth_day': forms.DateInput(attrs={'type':'date'})と設定すると更新画面で日付が入力できる
ユーザプロファイルを更新する
UserProfileUpdateViewを作成する
# users.views.py
from django.contrib.auth.views import LoginView, LogoutView
from django.views.generic import CreateView, UpdateView
from .forms import RegisterForm, UsersProfileForm
from .models import UserProfile
from django.utils.decorators import method_decorator
from django.contrib.auth.decorators import login_required
from django.urls import reverse
class RegisterView(CreateView):
template_name = 'users/register.html'
form_class = RegisterForm
success_url = '/'
class UserLoginView(LoginView):
template_name = 'users/login.html'
class UserLogoutView(LogoutView):
template_name = 'users/login.html'
@method_decorator(login_required(login_url='/users/login'), name='dispatch')
class UserProfileUpdateView(UpdateView):
model = UserProfile
template_name = 'users/profile-update.html'
form_class = UsersProfileForm
def form_valid(self, form):
form.instance.user = self.request.user
form.save()
return super().form_valid(form)
def get_success_url(self):
return reverse('users:update_profile', kwargs={'pk':self.object.id})
# 別ユーザのIDがlocalhost:8000/users/update-profile/idのidに入力されたらホームにリダイレクトする
def get(self, request, *args, **kwargs):
self.object = self.get_object()
if self.object.user != request.user:
return HttpResponseRedirect('/')
return super(UserProfileUpdateView,self).get(request, *args, **kwargs)
post-update.htmlを使ってuser-update.htmlを作成する
<!-- templates/users/user-update.html -->
{% extends 'base.html'%}
{% load static %}
{% load crispy_forms_tags %}
{% block content %}
<!--================Home Banner Area =================-->
<section class="banner_area">
<div class="banner_inner d-flex align-items-center">
<div class="overlay bg-parallax" data-stellar-ratio="0.9" data-stellar-vertical-offset="0" data-background=""></div>
<div class="container">
<div class="banner_content text-center">
<h2>プロファイル編集</h2>
<div class="page_link">
<a href="{% url 'index' %}">Home</a>
</div>
</div>
</div>
</div>
</section>
<!--================End Home Banner Area =================-->
<!--================Blog Area =================-->
<section class="blog_area p_120">
<div class="container">
<div class="row">
<div class="col-lg-8">
<h2 style="text-align: center; color:blue">プロファイル編集</h2>
<div class="blog_left_sidebar">
{% crispy form %}
</div>
</div>
{% include 'right_side.html' %}
</div>
</div>
</section>
{% endblock %}
ユーザプロファイルを表示する
# users.views.py
from django.views.generic import CreateView, UpdateView, ListView
from posts.models import Post
@method_decorator(login_required(login_url='/users/login'), name='dispatch')
class UserProfileView(ListView):
template_name = 'users/my-profile.html'
model = Post
context_object_name = 'userposts'
paginate_by = 5
def get_context_data(self, **kwargs):
context = super(UsersProfileView,self).get_context_data(**kwargs)
context['userprofile'] = UserProfile.objects.get(user=self.request.user)
return context
def get_queryset(self):
return Post.objects.filter(user=self.request.user).order_by('-publishing_date')
プロファイルのページmy-profile.htmlをcategory_detail.htmlをコピーして作成する
名前:{{ userprofile.user.username|title}}で先頭の名前が大文字になる
自己紹介:{{ userprofile.bio|linebreaks }}で改行が有効になる
<!-- templates/users/my-profile.html -->
{% extends 'base.html'%}
{% load static %}
{% block content %}
<!--================Home Banner Area =================-->
<section class="banner_area">
<div class="banner_inner d-flex align-items-center">
<div class="overlay bg-parallax" data-stellar-ratio="0.9" data-stellar-vertical-offset="0" data-background=""></div>
<div class="container">
<div class="banner_content text-center">
<h2>プロファイル</h2>
<div class="page_link">
<img src="{{ userprofile.image.url }}" alt="">
<h4 style="color:white">名前:{{ userprofile.user.username|title}}</h4>
<h4 style="color:white">誕生日:{{ userprofile.birth_day }}</h4>
<h4 style="color:white">自己紹介:{{ userprofile.bio|linebreaks }}</h4>
<a href="{% url 'users:update_profile' pk=userprofile.id %}" class="genric-btn info circle" style="margin-bottom:30px">変更</a>
</div>
</div>
</div>
</div>
</section>
<!--================End Home Banner Area =================-->
<!--================Blog Area =================-->
<section class="blog_area p_120">
<div class="container">
<div class="row">
<div class="col-lg-8">
<div class="blog_left_sidebar">
{% for post in userposts %}
<article class="blog_style1">
<div class="blog_img">
<img class="img-fluid" src={{ post.image.url }} alt="">
</div>
<div class="blog_text">
<div class="blog_text_inner">
{% for tag in post.tag.all %}
<a class="cat" href="#">{{ tag.title }}</a>
{% endfor %}
<a href="{% url 'detail' pk=post.id %}"><h4>{{ post.title | truncatechars:150}}</h4></a>
<p>{{ post.content }}</p>
<div class="date">
<a href="#"><i class="fa fa-calendar" aria-hidden="true"></i>{{post.publishing_date}}</a>
<a href="#"><i class="fa fa-comments-o" aria-hidden="true"></i> 05</a>
</div>
</div>
</div>
</article>
{% endfor %}
{% if is_paginated %}
<nav class="blog-pagination justify-content-center d-flex">
<ul class="pagination">
{% if page_obj.has_previous %}
<li class="page-item">
<a href="?page={{ page_obj.has_previous_page_number }}" class="page-link" aria-label="Previous">
<span aria-hidden="true">
<span class="lnr lnr-chevron-left"></span>
</span>
</a>
</li>
{% else %}
<li class="page-item disabled">
<a href="#" class="page-link" aria-label="Previous">
<span aria-hidden="true">
<span class="lnr lnr-chevron-left"></span>
</span>
</a>
</li>
{% endif %}
{% for i in paginator.page_range %}
{% if page_obj.number == i %}
<li class="page-item active"><a href="#" class="page-link">{{ i }}</a></li>
{% else %}
<li class="page-item"><a href="?page={{ i }}" class="page-link">{{ i }}</a></li>
{% endif %}
{% endfor %}
{% if page_obj.has_next %}
<li class="page-item">
<a href="?page={{ page_obj.has_next_page_number }}" class="page-link" aria-label="Next">
<span aria-hidden="true">
<span class="lnr lnr-chevron-right"></span>
</span>
</a>
</li>
{% else %}
<li class="page-item disabled">
<a href="#" class="page-link" aria-label="Next">
<span aria-hidden="true">
<span class="lnr lnr-chevron-right"></span>
</span>
</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
</div>
</div>
{% include 'right_side.html' %}
</div>
</div>
</section>
<!--================Blog Area =================-->
{% endblock %}
ユーザ別投稿(User Posts)を表示する
# users/views.py
class UserPostView(ListView):
template_name='users/user-post.html'
model = Post
context_object_name='posts'
paginate_by = 5
def get_queryset(self):
return Post.objects.filter(user=self.kwargs['pk'])
pathをurls.pyに追加する
# urls.py
from django.urls import path
from .views import *
from django.contrib.auth import views as authViews
from django.urls import reverse_lazy
app_name="users"
urlpatterns = [
path('register/', RegisterView.as_view(), name="register"),
path('login/', UserLoginView.as_view(), name="login"),
path('logout/', UserLogoutView.as_view(), name="logout"),
path('myprofile/', UserProfileView.as_view(), name="myprofile"),
path('<str:pk>/', UserPostView.as_view(), name="user_posts"), # added
path('update-profile/<str:pk>/', UserProfileUpdateView.as_view(), name="update_profile"),
path('password-change/', authViews.PasswordChangeView.as_view(success_url=reverse_lazy('users:password_change_done')), name="password_change"),
path('password-change-done/', authViews.PasswordChangeDoneView.as_view(), name="password_change_done")
]
投稿の詳細ページでクリックするとユーザ別投稿のページに遷移するように設定
<!-- posts/detail.html -->
<div class="float-right">
<div class="media">
<div class="media-body">
<h5><a href="{% url 'users:user_posts' single.user.pk %}"> {{ single.user.username }}</a></h5>
<p>{{ single.publishing_date }}</p>
</div>
<div class="d-flex">
<img src="{% static "img/blog/user-img.jpg" %}" alt="">
</div>
</div>
</div>
ユーザ別投稿のページのhmlを作成する(category_detail.htmlからコピー)
<!-- users/user-post.html -->
{% extends 'base.html'%}
{% load static %}
{% block content %}
<!--================Home Banner Area =================-->
<section class="banner_area">
<div class="banner_inner d-flex align-items-center">
<div class="overlay bg-parallax" data-stellar-ratio="0.9" data-stellar-vertical-offset="0" data-background=""></div>
<div class="container">
<div class="banner_content text-center">
<h2>ユーザ別投稿</h2>
<div class="page_link">
<a href="{% url 'index' %}">Home</a>
</div>
</div>
</div>
</div>
</section>
<!--================End Home Banner Area =================-->
<!--================Blog Area =================-->
<section class="blog_area p_120">
<div class="container">
<div class="row">
<div class="col-lg-8">
<div class="blog_left_sidebar">
{% for post in posts %}
<article class="blog_style1">
<div class="blog_img">
<img class="img-fluid" src={{ post.image.url }} alt="">
</div>
<div class="blog_text">
<div class="blog_text_inner">
{% for tag in post.tag.all %}
<a class="cat" href="#">{{ tag.title }}</a>
{% endfor %}
<a href="{% url 'detail' pk=post.id %}"><h4>{{ post.title | truncatechars:150}}</h4></a>
<p>{{ post.content }}</p>
<div class="date">
<a href="#"><i class="fa fa-calendar" aria-hidden="true"></i>{{post.publishing_date}}</a>
<a href="#"><i class="fa fa-comments-o" aria-hidden="true"></i> 05</a>
</div>
</div>
</div>
</article>
{% endfor %}
{% if is_paginated %}
<nav class="blog-pagination justify-content-center d-flex">
<ul class="pagination">
{% if page_obj.has_previous %}
<li class="page-item">
<a href="?page={{ page_obj.has_previous_page_number }}" class="page-link" aria-label="Previous">
<span aria-hidden="true">
<span class="lnr lnr-chevron-left"></span>
</span>
</a>
</li>
{% else %}
<li class="page-item disabled">
<a href="#" class="page-link" aria-label="Previous">
<span aria-hidden="true">
<span class="lnr lnr-chevron-left"></span>
</span>
</a>
</li>
{% endif %}
{% for i in paginator.page_range %}
{% if page_obj.number == i %}
<li class="page-item active"><a href="#" class="page-link">{{ i }}</a></li>
{% else %}
<li class="page-item"><a href="?page={{ i }}" class="page-link">{{ i }}</a></li>
{% endif %}
{% endfor %}
{% if page_obj.has_next %}
<li class="page-item">
<a href="?page={{ page_obj.has_next_page_number }}" class="page-link" aria-label="Next">
<span aria-hidden="true">
<span class="lnr lnr-chevron-right"></span>
</span>
</a>
</li>
{% else %}
<li class="page-item disabled">
<a href="#" class="page-link" aria-label="Next">
<span aria-hidden="true">
<span class="lnr lnr-chevron-right"></span>
</span>
</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
</div>
</div>
{% include 'right_side.html' %}
</div>
</div>
</section>
<!--================Blog Area =================-->
{% endblock %}
ユーザリストを作成する
<!-- templates/base.html -->
<ul class="nav navbar-nav menu_nav">
<li class="nav-item active"><a class="nav-link" href="{% url 'index' %}">ホーム</a></li>
<li class="nav-item"><a class="nav-link" href="category.html">カテゴリー</a></li>
<li class="nav-item"><a class="nav-link" href="{% url 'users:user_list' %}">ユーザーリスト</a></li> # Changed
{% if user.is_authenticated %}
<li class="nav-item submenu dropdown">
<a href="#" class="nav-link dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">{{user.username}}</a>
<ul class="dropdown-menu">
<li class="nav-item"><a class="nav-link" href="{% url 'create_post' %}">投稿の追加</a></li>
<li class="nav-item"><a class="nav-link" href="{% url 'users:password_change' %}">パスワードの変更</a></li>
<li class="nav-item"><a class="nav-link" href="{% url 'users:myprofile' %}">プロフィール</a></li>
<li class="nav-item"><a class="nav-link" href="{% url 'users:logout' %}">ログアウト</a></li>
</ul>
</li>
# users/views.py
class UserListView(ListView):
template_name ='users/user-list.html'
model = UserProfile
context_object_name='profiles'
paginate_by = 5
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
return context
ユーザーリストのhtmlを作成する
<!-- templates/users/user-list.html -->
{% extends 'base.html'%}
{% load static %}
{% block content %}
<!--================Home Banner Area =================-->
<section class="banner_area">
<div class="banner_inner d-flex align-items-center">
<div class="overlay bg-parallax" data-stellar-ratio="0.9" data-stellar-vertical-offset="0" data-background=""></div>
<div class="container">
<div class="banner_content text-center">
<h2>ユーザーリスト</h2>
<div class="page_link">
<a href="{% url 'index' %}">Home</a>
<a href="#">{{ category.title }}</a>
</div>
</div>
</div>
</div>
</section>
<!--================End Home Banner Area =================-->
<!--================Blog Area =================-->
<section class="blog_area p_120">
<div class="container">
<div class="row">
<div class="col-lg-8">
<div class="blog_left_sidebar" style="text-align:center">
{% for profile in profiles %}
<div class="container" style="border-style:solid; border-width; 2px; border-color:green; margin-bottom:5px;">
<img src="{{ profile.image.url }}" alt="">
<h4>{{ profile.user.username }}</h4>
<p>{{ profile.bio }}</P>
<p>{{ profile.birth_day }}</P>
</div>
{% endfor %}
{% if is_paginated %}
<nav class="blog-pagination justify-content-center d-flex">
<ul class="pagination">
{% if page_obj.has_previous %}
<li class="page-item">
<a href="?page={{ page_obj.has_previous_page_number }}" class="page-link" aria-label="Previous">
<span aria-hidden="true">
<span class="lnr lnr-chevron-left"></span>
</span>
</a>
</li>
{% else %}
<li class="page-item disabled">
<a href="#" class="page-link" aria-label="Previous">
<span aria-hidden="true">
<span class="lnr lnr-chevron-left"></span>
</span>
</a>
</li>
{% endif %}
{% for i in paginator.page_range %}
{% if page_obj.number == i %}
<li class="page-item active"><a href="#" class="page-link">{{ i }}</a></li>
{% else %}
<li class="page-item"><a href="?page={{ i }}" class="page-link">{{ i }}</a></li>
{% endif %}
{% endfor %}
{% if page_obj.has_next %}
<li class="page-item">
<a href="?page={{ page_obj.has_next_page_number }}" class="page-link" aria-label="Next">
<span aria-hidden="true">
<span class="lnr lnr-chevron-right"></span>
</span>
</a>
</li>
{% else %}
<li class="page-item disabled">
<a href="#" class="page-link" aria-label="Next">
<span aria-hidden="true">
<span class="lnr lnr-chevron-right"></span>
</span>
</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
</div>
</div>
{% include 'right_side.html' %}
</div>
</div>
</section>
<!--================Blog Area =================-->
{% endblock %}
# users/urls.py
from django.urls import path
from .views import *
from django.contrib.auth import views as authViews
from django.urls import reverse_lazy
app_name="users"
urlpatterns = [
path('', UserListView.as_view(), name="user_list"), #added
path('register/', RegisterView.as_view(), name="register"),
path('login/', UserLoginView.as_view(), name="login"),
path('logout/', UserLogoutView.as_view(), name="logout"),
path('myprofile/', UserProfileView.as_view(), name="myprofile"),
path('<int:pk>/', UserPostView.as_view(), name="user_posts"),
path('update-profile/<str:pk>/', UserProfileUpdateView.as_view(), name="update_profile"),
path('password-change/', authViews.PasswordChangeView.as_view(success_url=reverse_lazy('users:password_change_done')), name="password_change"),
path('password-change-done/', authViews.PasswordChangeDoneView.as_view(), name="password_change_done")
]
メニューバーにカテゴリーリストを表示する
<div class="collapse navbar-collapse offset" id="navbarSupportedContent">
<ul class="nav navbar-nav menu_nav">
<li class="nav-item active"><a class="nav-link" href="{% url 'index' %}">ホーム</a></li>
<li class="nav-item submenu dropdown">
<a href="#" class="nav-link dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">カテゴリー</a>
<ul class="dropdown-menu">
{% post_categories as categories %}
{% for category in categories %}
<li class="nav-item"><a class="nav-link" href="{% url 'category_detail' category.id %}">{{ category.title }}</a></li>
{% endfor %}
</ul>
</li>
<li class="nav-item"><a class="nav-link" href="{% url 'users:user_list' %}">ユーザーリスト</a></li>
img class="img-fluid" src={{ post.image.url }} alt="" style="width:100%"
style="width:100%" で画像の幅を広げる
メッセージを表示する
UserProfileUpdateViewでSuccessMessageMixinを追加する
# users/views.py
@method_decorator(login_required(login_url='/users/login'), name='dispatch')
class UserProfileUpdateView(SuccessMessageMixin, UpdateView):
model = UserProfile
template_name = 'users/profile-update.html'
form_class = UsersProfileForm
success_message ="プロフィールが更新されました"
def form_valid(self, form):
form.instance.user = self.request.user
form.save()
return super(UserProfileUpdateView,self).form_valid(form)
def get_success_url(self):
return reverse('users:update_profile', kwargs={'pk':self.object.id})
# 別ユーザのIDがlocalhost:8000/users/update-profile/idのidに入力されたらホームにリダイレクトする
def get(self, request, *args, **kwargs):
self.object = self.get_object()
if self.object.user != request.user:
return HttpResponseRedirect('/')
return super(UserProfileUpdateView,self).get(request, *args, **kwargs)
Django messagesで検索
Sample: https://docs.djangoproject.com/en/4.1/ref/contrib/messages/
{% if messages %}
<ul class="messages">
{% for message in messages %}
<li{% if message.tags %} class="{{ message.tags }}"{% endif %}>{{ message }}</li>
{% endfor %}
</ul>
{% endif %}
<!-- templates/messages.html -->
{% if messages %}
{% for message in messages %}
<div class="alert alert-success alert-dismissible">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button>
{{ message }}
</div>
{% endfor %}
{% endif %}
<!-- templates/base.html -->
<body>
<!--================Header Menu Area =================-->
<header class="header_area">
{% include 'messages.html' %} # added
<div class="logo_part">
<div class="container">
<a class="logo" href="#"><img src="{% static "img/logo.png" %}" alt=""></a>
</div>
</div>