はじめに
いいね!機能はライブラリで提供されるようなものではないので基本的に自作する必要がある。
例えば python,djangoによるいいねボタンの作り方 は、当該記事の筆者も言っているとおり、データベースに更新はかけるものの、単純に form の機能でカウントアップするだけのものです(何回も押せてしまう)。今回つくるものは、いわゆるfacebook式のいいね!です。
実際のサンプルはリポジトリをみてね
2023年に入ってからサイトをいろいろ改修しててその際に「なんかわかりにくいんだよなー」ってのがきっかけでリファクタリングしようとしたら意外に手こずっちゃったけどいい勉強になった。わかりやすくなったと思う
検索材料
google検索: django いいね ajax
Djangoでいいね機能をAjax通信で実装 (IPアドレスで連打を防止)
成果物のイメージ
要はシステムがユーザーIDを保持していて、いいね!は ON
or OFF
の二者択一。これ Ajax をよく知ってる人は簡単かもしれないけど結構総花的な知識が必要で難しいんですよ。。。サンプルを調べても無駄に複雑に作ってあったりして。だから可能な限りミニマルな形に削りました。
処理の流れのイメージ
今回は「いいね!」の機能に注力するため、Djangoの基本的な部分は省略しています。まぁ情報過多になっちゃうししょうがないよね。なのでDjango基礎は「一気通貫 Django startup ~ローカル環境にグラフアプリを添えて~」を見てください。
さて、下図は左下の chrome アイコンから始まる、処理の流れのイメージだ。
Userを作成
少なくともユーザがひとりいる状態にしてください。今回はスーパーユーザだけにします。
superuser作成
項目 | 値 |
---|---|
ユーザー名 | yoshi |
メールアドレス | yoshi@gmail.com |
(appname)\mysite> python manage.py createsuperuser
ユーザー名 (leave blank to use 'yoshi'):
メールアドレス: yoshi@gmail.com
Password:
Password (again):
このパスワードは ユーザー名 と似すぎています。
このパスワードは短すぎます。最低 8 文字以上必要です。
Bypass password validation and create user anyway? [y/N]: y
Superuser created successfully.
(appname)\mysite>
Articles(記事)とLikesのテーブルを作ろう
model
:
class Articles(models.Model):
"""いいね!機能つきの記事"""
title = models.CharField(verbose_name='タイトル', max_length=200)
note = models.TextField(verbose_name='投稿内容')
user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE)
created_at = models.DateTimeField('公開日時', auto_now_add=True)
@staticmethod
def with_state(user_id: int) -> QuerySet:
return Articles.objects.annotate(
likes_cnt=Count('likes'),
liked_by_me=Case(
When(id__in=Likes.objects.filter(user_id=user_id).values('articles_id'),
then=1),
default=0,
output_field=IntegerField()
)
)
class Likes(models.Model):
"""いいね"""
articles = models.ForeignKey('Articles', on_delete=models.CASCADE)
user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
constraints = [
models.UniqueConstraint(
fields=['articles_id', 'user_id'],
name='articles_user_unique'
)
]
:
migrate
(appname)\mysite> python manage.py makemigrations appname
Migrations for '(appname)':
(appname)\migrations\0003_articles_likes.py
- Create model Articles
- Create model Likes
(appname)\mysite> python manage.py migrate
Operations to perform:
Apply all migrations: admin, auth, contenttypes, sessions, appname
Running migrations:
Applying appname.0003_articles_likes... OK
確認
ArticlesとLikesのテーブルができた!(まだなんの処理とも紐付いてないよ)
ダミーの記事をつくっておく。dbeaverとかmysqlworkbenchなどのクライアントを使って画面上からレコード追加しちゃうほうがかんたん
urls.py
app_name = 'xxx'
urlpatterns = [
path('', index, name='index'),
path('likes/create/<int:article_id>/<int:user_id>/', LikesCreateView.as_view(), name='likes_create'),
path('likes/delete/<int:article_id>/<int:user_id>/', LikesDeleteView.as_view(), name='likes_delete'),
path('article/create/', ArticleCreateView.as_view(), name="article_create"),
:
]
views.py
def index(request):
:
login_user = User.objects.filter(email=request.user).first()
login_id = None
if login_user:
login_id = login_user.id
# TODO: articlesは試作のため3投稿のみ
context = {
:
'articles': Articles.with_state(login_id).annotate(user_name=F('user__email')).order_by('-created_at')[:3],
:
}
index.html
<!-- index.html -->
<div class="col-sm">
<h2>nice! function</h2>
<h6>'like' function</h6>
{% for article in articles %}
<div class="card" style="width: 18rem;">
<div class="card-header">
{{ article.title }}
</div>
<ul class="list-group list-group-flush">
<li class="list-group-item">
<p style="font-size: small">{{ article.note | truncatechars:80 }}</p>
<p class="author text-right text-secondary" style="font-size: xx-small">投稿者: {{ article.user_name | truncatechars:3 }} さん</p>
<div class="card-body">
<button
type="button"
class="like_toggle btn btn-outline-secondary {{ article.liked_by_me|yesno:'active,' }}"
data-article-id="{{ article.id }}"
data-liked-by-me="{{ article.liked_by_me }}"
>いいね!<span>({{ article.likes_cnt }})</span></button>
</div>
</li>
</ul>
</div>
{% endfor %}
<a href="{% url 'vnm:article_create' %}" class="btn btn-default btn-sm" role="button">post!</a>
</div>
:
<script lang="js">
const likeToggles = document.getElementsByClassName('like_toggle');
for (let i = 0; i < likeToggles.length; i++) {
likeToggles[i].addEventListener('click', () => {
const articleId = likeToggles[i].dataset.articleId
const userId = {{ user.id }}
if (!userId) {
location.href = myurl.login
}
const createOrDelete = parseInt(likeToggles[i].dataset.likedByMe) ? 'delete' : 'create'
fetch(`${myurl.base}likes/${createOrDelete}/${articleId}/${userId}/`, {
method: 'POST',
headers: {
"Content-Type": "application/json; charset=utf-8",
"X-CSRFToken": Cookies.get('csrftoken')
},
body: JSON.stringify({"status": "requested from javascript."})
})
.then(response => response.json())
.then(json => {
likeToggles[i].getElementsByTagName('span')[0].innerHTML = `(${json.likes_cnt})`
likeToggles[i].dataset.likedByMe = json.liked_by_me
likeToggles[i].classList.remove('active')
if (json.liked_by_me) {
likeToggles[i].classList.add('active')
}
})
.catch((error) => {
console.error('Ooops!! There has been a problem with your fetch operation:', error)
})
})
}
</script>