はじめに
いいね!機能はライブラリで提供されるようなものではないので基本的に自作する必要がある。
例えば python,djangoによるいいねボタンの作り方 は、当該記事の筆者も言っているとおり、データベースに更新はかけるものの、単純に form の機能でカウントアップするだけのものです(何回も押せる)。今回つくるものは、いわゆるfacebook式のいいね!です。
検索材料
google検索: django いいね ajax
Djangoでいいね機能をAjax通信で実装 (IPアドレスで連打を防止)
成果物のイメージ
要はシステムがユーザーIDを保持していて、いいね!は ON or OFF の二者択一。これ Ajax をよく知ってる人は簡単かもしれないけど結構総花的な知識が必要で難しいんですよ。。。サンプルを調べても無駄に複雑に作ってあったりして。だから可能な限りミニマルな形に削りました。
処理の流れのイメージ
今回は「いいね!」の機能に注力するため、Djangoの基本的な部分は省略しています。まぁ情報過多になっちゃうししょうがないよね。なのでDjango基礎は「一気通貫 Django startup ~ローカル環境にグラフアプリを添えて~」を見てください。
さて、下図は左下の chrome アイコンから始まる、処理の流れのイメージだ。
Djangoアプリケーション用のUserを作成
アプリケーションのなかだけのユーザーなので、なんでもいいよ
superuser作成
項目 | 値 |
---|---|
ユーザー名 | yoshi |
メールアドレス | yoshi@gmail.com |
basic_django_graph\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.
basic_django_graph\mysite>
確認
ユーザを増やす
サーバー起動してadminページへアクセス
basic_django_graph\mysite> python manage.py runserver
ユーザを増やす
ユーザー名 | パスワード |
---|---|
guest1 | guest1guest1 |
guest2 | guest2guest2 |
確認
Articles(記事)とLikesのテーブルを作ろう
model追記
+
の行をソースに追記
from django.db import models
+ from django.contrib.auth import get_user_model
# クラス名がテーブル名です
class DjangoTestTable(models.Model):
# ここに定義したものがフィールド項目です
month_code = models.CharField(default='XXX', max_length=3) # Jun Feb など
sales = models.IntegerField(default=0)
pub_date = models.DateTimeField('date published')
+ 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)
+ 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)
テーブルの定義を変更
basic_django_graph\mysite> python manage.py makemigrations test_chartjs
Migrations for 'test_chartjs':
test_chartjs\migrations\0003_articles_likes.py
- Create model Articles
- Create model Likes
basic_django_graph\mysite> python manage.py migrate
Operations to perform:
Apply all migrations: admin, auth, contenttypes, sessions, test_chartjs
Running migrations:
Applying test_chartjs.0003_articles_likes... OK
確認
ArticlesとLikesのテーブルができた!(まだなんの処理とも紐付いてないよ)
ダミーの記事をつくる
INSERT INTO db.test_chartjs_articles (id, title, note, user_id, created_at) VALUES (1, 'test', 'guestが書き込みました', 2, now());
INSERT INTO db.test_chartjs_articles (id, title, note, user_id, created_at) VALUES (2, 'いいね機能がつきました', '便利ですね', 2, now());
ダミーのいいねをつくる
INSERT INTO db.test_chartjs_likes (id, created_at, articles_id, user_id) VALUES (1, now(), 1, 2);
INSERT INTO db.test_chartjs_likes (id, created_at, articles_id, user_id) VALUES (2, now(), 2, 2);
確認
リクエストが発生した際の、次の処理への交通整理部分
必要なパッケージ
sqlalchemy
と pandas
basic_django_graph\mysite> pip install sqlalchemy pandas
urls.py
from django.urls import path
# STEP1: 現在のフォルダの「views.py」を import する!さっき編集したやつ!
from . import views
# STEP2: views.py には「index」という関数を作りましたね!それを呼んでます
urlpatterns = [
# index.htmlがリクエストされたときは views.index の処理に
path('', views.index, name='index'),
+ # STEP3: いいね!がリクエストされたときは views.likes の処理に 引数[ユーザID, 記事ID] を渡します
+ path('likes/<int:user_id>/<int:article_id>', views.likes, name='likes'),
]
views.py
"""子供のurls.pyがこの処理を呼び出します"""
from django.shortcuts import render
from .models import DjangoTestTable
+ from sqlalchemy import create_engine
+ import pandas as pd
+ from .models import Likes, Articles
+ from django.http import JsonResponse
def index(request):
"""いわばhtmlのページ単位の構成物です"""
# 日付を降順に表示するクエリ
ret = DjangoTestTable.objects.order_by('-pub_date')
# (追加ここから)
# mysql
con_str = 'mysql+mysqldb://python:python123@127.0.0.1/db?charset=utf8&use_unicode=1'
con = create_engine(con_str, echo=False).connect()
# likes
user_id = 1 # todo: where user_id
articles = pd.read_sql_query(
'''
SELECT a.id, a.title, a.note, COALESCE(qry1.is_like, 0) is_like, qry2.likes_cnt
FROM test_chartjs_articles a
LEFT JOIN (
SELECT articles_id, 1 is_like
FROM test_chartjs_likes WHERE user_id = {0}) qry1 ON a.id = qry1.articles_id
LEFT JOIN (
SELECT articles_id, COUNT(user_id) likes_cnt
FROM test_chartjs_likes GROUP BY articles_id) qry2 ON a.id = qry2.articles_id
ORDER BY a.id;
'''.format(user_id), con)
# (追加ここまで)
- context = {'latest_list': ret}
+ context = {
+ 'latest_list': ret,
+ 'articles': articles,
+ }
# htmlとして返却します
return render(request, 'test_chartjs/index.html', context)
# ここから↓を全部追加
def likes(request, user_id, article_id):
"""いいねボタンをクリック"""
if request.method == 'POST':
query = Likes.objects.filter(user_id=user_id, articles_id=article_id)
if query.count() == 0:
likes_tbl = Likes()
likes_tbl.user_id = user_id
likes_tbl.articles_id = article_id
likes_tbl.save()
else:
query.delete()
# response json
return JsonResponse({"status": "responded by views.py"})
index.html
<!DOCTYPE html>
{% load static %}
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<!-- css -->
<link rel="stylesheet" href="{% static 'test_chartjs/css/reset.css' %}">
<link rel="stylesheet" href="{% static 'test_chartjs/css/index.css' %}">
<!-- chart.js -->
<script src="https://cdn.jsdelivr.net/npm/chart.js@2.8.0"></script>
<!-- likes -->
<script src="{% static 'test_chartjs/js/ajax.js' %}" charset="utf-8"></script>
<script src="https://cdn.jsdelivr.net/npm/js-cookie@beta/dist/js.cookie.min.js"></script>
<!-- for ajax -->
<script>let myurl = {"base": "{% url 'index' %}"};</script>
</head>
<body>
# (追加ここから)
<!-- likes -->
<div id="container">
<div id="main">
<!-- likes -->
<h2>いいね!機能</h2>
{% for idx, row in articles.iterrows %}
<div class="article">
<span class="title">{{ row.title }}</span>
<p>{{ row.note }}</p>
<ul>
{% if row.is_like == 1 %}
<li><a role="button" aria-pressed="true" onclick="likes(event, '1', '{{ row.id }}')">いいね!<span>({{ row.likes_cnt }})</span></a></li>
{% else %}
<li><a role="button" aria-pressed="false" onclick="likes(event, '1', '{{ row.id }}')">いいね!<span>({{ row.likes_cnt }})</span></a></li>
{% endif %}
</ul>
</div>
{% endfor %}
</div>
</div>
# (追加ここまで)
<!-- Django to Javascript -->
<script>
var month_code = [];
var sales = [];
</script>
{% if latest_list %}
{% for i in latest_list %}
<script>
month_code.push("{{ i.month_code }}")
sales.push("{{ i.sales }}");
</script>
{% endfor %}
{% else %}
<p>latest_listのデータがありませんでした</p>
{% endif %}
<!-- chart.js -->
<canvas id="myChart"></canvas>
<script>
var ctx = document.getElementById('myChart').getContext('2d');
var chart = new Chart(ctx, {
// The type of chart we want to create
type: 'line',
// The data for our dataset
data: {
labels: month_code,
datasets: [{
label: 'My First dataset',
backgroundColor: 'rgb(255, 99, 132)',
borderColor: 'rgb(255, 99, 132)',
data: sales
}]
},
// Configuration options go here
options: {}
});
</script>
</body>
</html>
index.css
とりあえずのcssです。
h2 {
border-left: 5px solid #ff5900;
background-color: #fdece3;
margin-top: 30px;
margin-bottom: 10px;
padding: 10px;
font-size: 20px;
color: gray;
}
h2::before {
font-family: "Font Awesome 5 Free";
content: '\f00c';
font-weight: 900;
padding: 10px;
}
/* article */
.article a::before {
font-family: "Font Awesome 5 Free";
content: '\f164';
font-weight: 400;
padding: 10px;
}
.article ul {
margin: 0px;
padding: 0px;
}
.article li {
display: inline-block;
list-style: none;
padding: 10px;
user-select: none;
}
.article li:hover {
background-color: whitesmoke;
}
.article a {
text-decoration: none;
color: gray;
padding-right: 15px;
}
.article a span {
display: inline;
font-size: small;
color: inherit;
padding: 0px;
}
.article {
position: relative;
margin: 2em 0;
padding: 25px 10px 7px;
border: solid 2px #FFC107;
}
.article .title {
position: absolute;
display: inline-block;
top: -2px;
left: -2px;
padding: 0 9px;
height: 25px;
line-height: 25px;
font-size: 17px;
background: #FFC107;
color: #ffffff;
font-weight: bold;
}
.article p {
margin: 20px;
padding: 0px;
}
.article p.author {
font-size: small;
text-align: right;
color: lightgray;
}
reset.css
これは、お決まりなんや。ブラウザのcss初期設定は結構使いづらいのでresetをかける
【初心者必見!】リセットCSSとは?コピペで使えるオススメ6選!
/*
html5doctor.com Reset Stylesheet
v1.6.1
Last Updated: 2010-09-17
Author: Richard Clark - http://richclarkdesign.com
Twitter: @rich_clark
*/
html, body, div, span, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
abbr, address, cite, code,
del, dfn, em, img, ins, kbd, q, samp,
small, strong, sub, sup, var,
b, i,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, figcaption, figure,
footer, header, hgroup, menu, nav, section, summary,
time, mark, audio, video {
margin:0;
padding:0;
border:0;
outline:0;
font-size:100%;
vertical-align:baseline;
background:transparent;
}
body {
line-height:1;
}
article,aside,details,figcaption,figure,
footer,header,hgroup,menu,nav,section {
display:block;
}
nav ul {
list-style:none;
}
blockquote, q {
quotes:none;
}
blockquote:before, blockquote:after,
q:before, q:after {
content:'';
content:none;
}
a {
margin:0;
padding:0;
font-size:100%;
vertical-align:baseline;
background:transparent;
}
/* change colours to suit your needs */
ins {
background-color:#ff9;
color:#000;
text-decoration:none;
}
/* change colours to suit your needs */
mark {
background-color:#ff9;
color:#000;
font-style:italic;
font-weight:bold;
}
del {
text-decoration: line-through;
}
abbr[title], dfn[title] {
border-bottom:1px dotted;
cursor:help;
}
table {
border-collapse:collapse;
border-spacing:0;
}
/* change border colour to suit your needs */
hr {
display:block;
height:1px;
border:0;
border-top:1px solid #cccccc;
margin:1em 0;
padding:0;
}
input, select {
vertical-align:middle;
}
確認
basic_django_graph\mysite> python manage.py runserver
Ajax
function likes(event, user_id, article_id) {
console.log(user_id)
if(user_id != "None"){
fetch(myurl.base + 'likes/' + user_id + '/' + article_id, {
method: 'POST',
headers: {
"Content-Type": "application/json; charset=utf-8",
"X-CSRFToken": Cookies.get('csrftoken')
},
body: JSON.stringify({"status": "requested by javascript."})
}
)
.then(response => response.json())
.then(json => {
// json-value
console.log(json)
// state of 'like'
var is_pressed = (event.target.getAttribute("aria-pressed") === "true");
event.target.setAttribute("aria-pressed", !is_pressed);
// count of 'like' ±1
var tag_span = event.target.getElementsByTagName('span')[0];
coefficient = !is_pressed ? +1 : -1
cnt = tag_span.innerHTML.match(/\((.+)\)/)[1]; // e.g. (3) => 3
tag_span.innerHTML = tag_span.innerHTML.replace(cnt, parseInt(cnt) + coefficient);
})
} else {
location.href=myurl.login;
}
}