前回の記事まででユーザー認証機能の実装が完了しました。今回はタイムライン機能について実装しましょう。
改めて必要な機能について整理しましょう。
必要な機能について
①投稿一覧(タイムライン)
②投稿詳細
③投稿機能
④投稿編集機能
⑤返信機能
⑥ユーザーごとの投稿
最終的なサイトのデザイン等についてはこちらの記事を参照ください。
タイムライン機能の実装
ここまでの時点でディレクトリ構成は以下のようになっていると思います。app_sns/
├プロジェクト
├ manage.py
└ accounts(app_name)#前回の記事で実装
└ timelines(app_name)/
├ __init__.py
├ apps.py
├ models.py
├ tests.py
├ views.py
└ urls.py#なければ追記
└ templates/
└ static/
└ css/style.css
ここではまず以下のことをしてください。
①timelinesにforms.pyとcontext.pyを追加する。
②templatesに以下のhtmlファイルを追加する。
・top.html
・index.html
・detail.html
・form.html
・delete.html
・update.html
・comment.html
・userpost.html
③staticフォルダにjsというフォルダを作成し,その中にindex.jsを作成する。
コード全文
from django.urls import path
from . import views
app_name = 'timelines'
urlpatterns = [
path('top/', views.TopView.as_view(), name = 'top'),
path('', views.IndexView.as_view(), name = 'index'),
path('<int:pk>/', views.DetailView.as_view(), name = 'detail'),
path('create/', views.PostCreateView.as_view(), name = 'create'),
path('delete/<int:pk>/', views.PostDeleteView.as_view(), name = 'delete'),
path('update/<int:pk>/', views.PostUpdateView.as_view(), name = 'update'),
path('comment/<int:pk>/', views.CommentCreateView.as_view(), name = 'comment'),
path('userpost/<int:user>/', views.UserPostView.as_view(), name = 'userpost'),
]
from django.db import models
from accounts.models import User
# Create your models here.
class Post(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE ,verbose_name = 'ユーザー')
text = models.TextField(verbose_name = '本文')
created_at = models.DateTimeField(auto_now_add=True, verbose_name = '投稿日時')
def __str__(self):
return self.text
class Comment(models.Model):
text = models.TextField(verbose_name = 'コメント')
created_at = models.DateTimeField(auto_now_add=True, verbose_name = '返信日時')
target = models.ForeignKey(Post, on_delete=models.CASCADE, verbose_name = '対象の投稿')
user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name = 'ユーザー')
def __str__(self):
return self.text
from django import forms
from .models import Post, Comment
class PostCreateForm(forms.ModelForm):
class Meta:
model = Post
fields = ('text',)
class CommentCreateForm(forms.ModelForm):
class Meta:
model = Comment
fields = ('text',)
@method_decorator(login_required, name = 'dispatch')がついているビューはログイン状態のときのみ表示・機能するビューです。
新規作成や更新などの処理では、forms.pyで作成したModelFormを利用しています。
models.pyのPostテーブル内のtextのみが自由にDB登録でき、それ以外は自動で登録されます。(ユーザーはログインしているユーザー)
get_querysetやget_context_dataの考え方については詳細は別の記事(後日公開予定)で解説しますが、基本的にはDBから特定の情報を取得し、テンプレートにお渡し表示するという流れです。
form_validは送信時に使う関数で入力されているか判定するために利用します。
例えば新規投稿時にtextが空欄だったらエラーをはいて送信処理を中断します。
from django.shortcuts import render, get_object_or_404, redirect
from .models import Post, Comment
from .forms import PostCreateForm, CommentCreateForm
from django.urls import reverse_lazy
from django.views.generic import TemplateView, ListView, DetailView, CreateView, DeleteView, UpdateView
from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
from django.contrib import messages
from accounts.models import User
# Create your views here.
class TopView(TemplateView):
template_name = 'top.html'
@method_decorator(login_required, name = 'dispatch')
class IndexView(ListView):
template_name = 'index.html'
context_object_name = 'posts'
def get_queryset(self):
queryset = Post.objects.all().order_by('-created_at')
return queryset
@method_decorator(login_required, name = 'dispatch')
class DetailView(DetailView):
model = Post
template_name = 'detail.html'
@method_decorator(login_required, name = 'dispatch')
class PostCreateView(CreateView):
model = Post
form_class = PostCreateForm
template_name = 'form.html'
success_url = reverse_lazy('timelines:index')
def form_valid(self, form):
pd = form.save(commit=False)
pd.user = self.request.user
pd.save()
return super().form_valid(form)
@method_decorator(login_required, name = 'dispatch')
class PostDeleteView(DeleteView):
model = Post
template_name = 'delete.html'
success_url = reverse_lazy('timelines:index')
def delete(self,request, *args, **kwargs):
return super().delete(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
print(context)
return context
@method_decorator(login_required, name = 'dispatch')
class PostUpdateView(UpdateView):
model = Post
template_name = 'update.html'
form_class = PostCreateForm
success_url = reverse_lazy('timelines:index')
@method_decorator(login_required, name = 'dispatch')
class CommentCreateView(CreateView):
model = Comment
template_name = 'comment.html'
form_class = CommentCreateForm
success_url = reverse_lazy('timelines:detail')
def form_valid(self, form):
post_pk = self.kwargs['pk']
post = get_object_or_404(Post, pk=post_pk)
comment = form.save(commit=False)
comment.target = post
comment.user = self.request.user
comment.save()
return redirect('timelines:detail', pk=post.pk)
class UserPostView(ListView):
template_name = 'userpost.html'
def get_queryset(self):
user_id = self.kwargs['user']
queryset = Post.objects.filter(user = user_id)
return queryset
from timelines.models import Post
def common(request):
context = {
'user_list':Post.objects.all(),
}
return context
テンプレート
{% extends 'base.html'%}
{% load static %}
{% block contents %}
<div class = 'top-page'>
<div class = 'top-container'>
<h2>SNSAPP~no-title~へようこそ</h2>
<h5>日常の緩い出来事やポジティブな出来事を発信しよう!!</h5>
{% if user.is_authenticated %}
<div class = 'topbtn'>
<button class = 'btn-auth' onClick = "location.href = '{% url 'timelines:index' %}'">タイムラインを見よう!</a>
<button class = 'btn-auth' onClick = "location.href = '{% url 'timelines:create' %}'">コメントを投稿しよう!</a>
</div>
{% else %}
<div class = 'tab'>
<ul class = 'tab_menu'>
<li class = 'tab_menu-item is-active' data-tab = '01'>このサイトについて</li>
<li class = 'tab_menu-item' data-tab = '02'>Get Start</li>
</ul>
<div class = 'tab_panel'>
<div class = 'tab_panel-box tab_panel-box001 is-show' data-tab = '01'>
<h3>Concept</h3>
<p>SNSです。。。</p>
</div>
<div class = 'tab_panel-box tab_panel-box002' data-tab = '02'>
<p>アカウントをお持ちの方はこちら</p>
<button class = 'btn-auth' onClick = "location.href = '{% url 'accounts:login' %}'">ログインして投稿しよう!</button>
<br></br>
<p>アカウントを持っていない方はこちら</p>
<button class = 'btn-auth' onClick = "location.href = '{% url 'accounts:signup' %}'">アカウントを作成して投稿しよう!</button>
</div>
</div>
</div>
{% endif %}
</div>
</div>
<script src = '{% static 'js/index.js' %}'></script>
{% endblock %}
{% extends 'base.html'%}
{% load static %}
{% block contents %}
<div class = 'timeline'>
<h3>タイムライン</h3>
</div>
{% for post in posts %}
<div class="card">
<div class="card-header">
{{ post.user }}<span class = 'created_at'>{{ post.created_at }}</span>
</div>
<div class="card-body">
<p class="card-text">{{ post.text|truncatechars:30 }}</p>
<a href='{% url 'timelines:detail' post.pk %}' style="text-decoration: none;" class="btn-auth">投稿詳細</a>
<a href = '{% url 'timelines:userpost' post.user.id %}' style="text-decoration: none;" class = 'btn_auth'>
{{ post.user.username }}の投稿一覧へ</a>
</div>
</div>
{% endfor %}
{% endblock %}
{% extends 'base.html'%}
{% load static %}
{% block contents %}
<div class="card">
<div class="card-header">
{{ post.user }}<span class = 'created_at'>{{ post.created_at }}</span>
</div>
<div class="card-body">
<p class="card-text">{{ post.text }}</p>
<p>
{% if post.user == user %}
<a href = '{% url 'timelines:delete' post.pk %}'>削除する</a>
<a href = '{% url 'timelines:update' post.pk %}'>更新</a>
{% endif %}
</p>
<a class = 'btn-auth' onClick = "location.href = '{% url 'timelines:comment' post.pk %}'" style="text-decoration: none;">返信する</a>
<a href = '{% url 'timelines:userpost' post.user.id %}' style="text-decoration: none;" class = 'btn_auth'>
{{ post.user.username }}の投稿一覧へ</a>
</div>
</div>
<div class = 'comment'>
<h5>コメント一覧</h5>
</div>
{% for comment in post.comment_set.all %}
<div class="card">
<div class="card-header">
{{ comment.user }}<span class = 'created_at'>{{ comment.created_at }}</span>
</div>
<div class="card-body">
<p class="card-text">{{ comment.text }}</p>
<a class = 'btn-auth' onClick = "location.href = '{% url 'timelines:comment' post.pk %}'" style="text-decoration: none;">返信する</a>
<a href = '{% url 'timelines:userpost' comment.user.id %}' style="text-decoration: none;" class = 'btn_auth'>{{ comment.user.username }}の投稿一覧へ</a>
</div>
</div>
{% endfor %}
{% endblock %}
{% extends 'base.html'%}
{% load static %}
{% block contents %}
<div class = 'form-container'>
<div class = "form">
<form method = 'post' action = '{% url 'timelines:create' %}'>
{% csrf_token %}
<h2 style = 'text-align:center'>投稿</h2>
<div class = 'form-area'>
{% for field in form %}
<div class = 'userinfo'>
<div class = 'info'>
<label for = 'Username'><span>必須</span>投稿してください</label>
<div class="mb-3">
<textarea type="text" name = 'text' rows = '10' id = 'id_text'autocapitalize = 'none' autocomplete = 'username' class = 'form-control' placeholder = 'テキストを入力してください。' required autofocus></textarea>
</div>
</div>
</div>
<p>
{% if field.help_text %}
{{ field.help_text }}
{% endif %}
</p>
<p>{{ field.errors }}</p>
{% endfor %}
<input type = 'submit' onClick = 'return alertFunc()' class = 'btn_auth' value = '投稿する'>
{% if form.is_valid %}
<script>
function alertFunc() {
alert("投稿完了!タイムラインに戻ります。");
}
</script>
{% endif %}
</div>
</form>
</div>
</div>
{% endblock %}
{% extends 'base.html' %}
{% load static %}
{% block contents %}
<div class = 'form-container'>
<div class = "form">
<form method = 'post'>
{% csrf_token %}
<h2 style = 'text-align:center'>投稿</h2>
<div class = 'form-area'>
<div class = 'userinfo'>
<div class = 'info'>
<label for = 'Username'><span>必須</span>投稿してください</label>
<div class="mb-3">
{{ form.text }}
</div>
</div>
</div>
<input type = 'submit' onClick = 'return alertFunc()' class = 'btn_auth' value = '投稿する'>
<script>
function alertFunc() {
if(document.form.input01.value == ""){
return false;
} else {
alert("投稿完了!タイムラインに戻ります。");
return true;
}
}
</script>
</div>
</form>
</div>
</div>
{% endblock %}
{% extends 'base.html' %}
{% load static %}
{% block contents %}
<div class="card">
<div class="card-header">
{{ post.user }}<span class = 'created_at'>{{ post.created_at }}</span>
</div>
<div class="card-body">
<p class="card-text">{{ post.text }}</p>
<form method = 'post'>
{% csrf_token %}
<input type = 'submit' class = 'btn btn-primary' onclick = 'return alertFunc()' value = '削除する'/>
</form>
<script>
function alertFunc() {
var del = confirm("本当に削除しますか?");
if (del == true) {
return true;
} else {
location.href = '{% url 'timelines:index' %}'
return false;
}
}
</script>
</div>
</div>
{% endblock %}
{% extends 'base.html' %}
{% load static %}
{% block contents %}
<div class = 'form-container'>
<div class = "form">
<form method = 'post'>
{% csrf_token %}
<h2 style = 'text-align:center'>返信する</h2>
<div class = 'form-area'>
<div class = 'userinfo'>
<div class = 'info'>
<label for = 'Username'><span>必須</span>返信内容を入力してください</label>
<div class="mb-3">
{{ form.text }}
</div>
</div>
</div>
<input type = 'submit' value = '返信する' />
</div>
</div>
</div>
{% endblock %}
{% extends 'base.html' %}
{% load static %}
{% block contents %}
{% for post in object_list %}
<div class="card">
<div class="card-header">
{{ post.user }}<span class = 'created_at'>{{ post.created_at }}</span>
</div>
<div class="card-body">
<p class="card-text">{{ post.text|truncatechars:30 }}</p>
<a href='{% url 'timelines:detail' post.pk %}' style="text-decoration: none;" class="btn-auth">投稿詳細</a>
{% if post.user == user %}
<a href = '{% url 'timelines:delete' post.pk %}' class="btn btn-primary">削除する</a>
<a href = '{% url 'timelines:update' post.pk %}' class="btn btn-primary">更新</a>
{% endif %}
</div>
</div>
{% endfor %}
{% endblock %}
Staticファイルについて
ここまでで基本的な機能と動作はしますが少し味気のないサイトになるためCSSとJavaScriptを追加します。
body {
background-color: aquamarine;
font-family: Meirio;
}
.timeline {
width: 60%;
margin: 30px auto;
text-align: center;
}
.timeline h3 {
border-bottom: 1px solid black;
}
.comment {
margin: 20px auto;
width: 60%;
text-align: center;
}
.comment h5 {
border-bottom: 1px solid black;
}
.card {
width: 30%;
margin: 20px auto;
}
.created_at {
float: right;
}
.inputarea {
width: 40%;
margin: 0 auto;
}
.form {
background:#fff;
border-radius:6px;
padding:20px;
padding-top:30px;
width:60%;
margin:50px auto;
box-shadow:5px 5px 0px rgba(0,0,0,.1);
}
.resetform {
background:#fff;
border-radius:6px;
padding:20px;
padding-top:30px;
width:80%;
margin:50px auto;
box-shadow:5px 5px 0px rgba(0,0,0,.1);
}
.resetform h1 {
border-bottom: 1px solid black;
}
.userinfo {
margin-bottom: 30px;
text-align: left;
}
.info {
margin-bottom: 30px;
margin-top: 20px;
}
.info span {
background-color: rgb(218, 34, 34);
border-radius: 5px;
color: white;
padding: 5px;
}
.info input {
margin-top: 20px;
}
.btn_auth {
text-align: center;
width: 40%;
height: 60px;
border-radius: 40px;
background-color: rgb(222, 52, 173);
color: white;
box-shadow: 2px 2px 0px rgb(74, 73, 73);
}
.btn_auth:active {
top: 3px;
box-shadow: none;
}
.tab {
max-width: 800px;
margin: 0 auto;
margin-top: 60px;
}
.top-container h2, h5 {
margin-top: 50px;
text-align: center
}
.tab_menu {
display: flex;
align-items: flex-end;
justify-content: left;
min-height: 50px;
padding: 0;
margin: 0;
}
.tab_menu-item {
list-style: none;
width: 200px;
padding: 8px 5px;
text-align: center;
margin-right: 6px;
background-color: #cdcdcd;
border-top-left-radius: 10px;
border-top-right-radius: 10px;
cursor: pointer;
transition: all .3s;
}
.tab_menu-item:last-of-type {
margin-right: 0px;
}
.tab_menu-item.is-active {
background-color: rgba(48, 172, 249);
color: #ffffff;
padding: 12px 5px;
}
.tab_panel {
width: 100%;
}
.tab_panel-box {
padding: 10px 30px;
border-radius: 10px;
}
.tab_panel-box001 {
background-color: rgb(205, 246, 246);
display: none;
}
.tab_panel-box002 {
background-color: rgb(249, 227, 243);
display: none;
}
.tab_panel-box.is-show {
display: block;
}
.btn-auth {
text-align: center;
width: 40%;
height: 60px;
border-radius: 40px;
background-color: rgb(45, 175, 250);
color: white;
box-shadow: 2px 2px 0px rgb(74, 73, 73);
}
.btn-auth:hover {
background-color: rgb(247, 100, 1);
box-shadow: none;
}
.top-container {
text-align: center;
}
.top-container .btn-auth {
margin-top: 30px;
}
.form-area {
text-align: center;
margin: 0 auto;
width: 60%;
}
.form-container h2 {
border-bottom: 1px solid black;
}
.mb-3 {
margin-top: 30px;
}
.info h5 {
margin-bottom: 30px;
}
javascriptではtopページのタブメニュー実装に使用しています。
他にもテンプレート内に一部記載しておりアラートや確認メッセージに利用しています。
const tabs = document.getElementsByClassName('tab_menu-item');
for(let i = 0; i < tabs.length; i++) {
tabs[i].addEventListener('click', tabSwitch);
}
function tabSwitch(){
document.getElementsByClassName('is-active')[0].classList.remove('is-active');
this.classList.add('is-active');
document.getElementsByClassName('is-show')[0].classList.remove('is-show');
const arrayTabs = Array.prototype.slice.call(tabs);
const index = arrayTabs.indexOf(this);
document.getElementsByClassName('tab_panel-box')[index].classList.add('is-show');
};
まとめ
ここまででSNSサイトの作成は完了です。 Djangoを使えば以外に簡単にSNSサイトを作成できます。 上記コードの中でわからないこと等あればコメントで気軽に質問してください。コード全文はGitHubに記載しています参考にしてください