Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
Help us understand the problem. What is going on with this article?

Djangoでいいね!をつくる

はじめに

いいね!機能はライブラリで提供されるようなものではないので基本的に自作する必要がある。
例えば python,djangoによるいいねボタンの作り方 は、当該記事の筆者も言っているとおり、データベースに更新はかけるものの、単純に form の機能でカウントアップするだけのものです(何回も押せる)。今回つくるものは、いわゆるfacebook式のいいね!です。

検索材料

google検索: django いいね ajax
Djangoでいいね機能をAjax通信で実装 (IPアドレスで連打を防止)

成果物のイメージ

要はシステムがユーザーIDを保持していて、いいね!は ON or OFF の二者択一。これ Ajax をよく知ってる人は簡単かもしれないけど結構総花的な知識が必要で難しいんですよ。。。サンプルを調べても無駄に複雑に作ってあったりして。だから可能な限りミニマルな形に削りました。
image.png

処理の流れのイメージ

今回は「いいね!」の機能に注力するため、Djangoの基本的な部分は省略しています。まぁ情報過多になっちゃうししょうがないよね。なのでDjango基礎は「一気通貫 Django startup ~ローカル環境にグラフアプリを添えて~」を見てください。

さて、下図は左下の chrome アイコンから始まる、処理の流れのイメージだ。
image.png

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>

確認

image.png

ユーザを増やす

サーバー起動してadminページへアクセス

basic_django_graph\mysite> python manage.py runserver

http://127.0.0.1:8000/admin
image.png

ユーザを増やす

2ユーザ増やします
image.png
image.png

ユーザー名 パスワード
guest1 guest1guest1
guest2 guest2guest2

image.png

確認

3ユーザいます
image.png

Articles(記事)とLikesのテーブルを作ろう

model追記

+ の行をソースに追記

basic_django_graph/mysite/test_chartjs/models.py
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)

テーブルの定義を変更

makemigrationsとmigrateでテーブルの定義を変更
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のテーブルができた!(まだなんの処理とも紐付いてないよ)
image.png

ダミーの記事をつくる

MySQLWorkbenchに以下SQLをコピペして実行すると記事が2つできる
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());

ダミーのいいねをつくる

いいねを1づつ
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);

確認

image.png

リクエストが発生した際の、次の処理への交通整理部分

必要なパッケージ

sqlalchemypandas

basic_django_graph\mysite> pip install sqlalchemy pandas

urls.py

basic_django_graph/mysite/test_chartjs/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

basic_django_graph/mysite/test_chartjs/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

basic_django_graph/mysite/test_chartjs/templates/test_chartjs/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です。

basic_django_graph/mysite/test_chartjs/static/test_chartjs/css/index.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選!

basic_django_graph/mysite/test_chartjs/static/test_chartjs/css/reset.css
/*
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

image.png

Ajax

basic_django_graph/mysite/test_chartjs/static/test_chartjs/js/ajax.js
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;
    }

}
YoshitakaOkada
フリーランスになってよ(自衛隊→IT(銀行, 証券)+DJ+居酒屋+運送仕分け→旅館+農業→IT(重工業, Web広告))。IT技術の社会実装研究を起点に第一次産業への興味へ派生しています。最近はpython×株にお熱。かなり雑多にアウトプットしますが、じつは最終的にひとつのゴールに向かっています。
https://www.henojiya.net/
veterans
退職予定自衛官、元自衛官のキャリア支援や社会的価値向上に繋げる活動を行うコミュニティです。民間企業で活躍する自衛官OB、今後民間企業に再就職予定自衛官、自衛隊入隊を希望する方々の情報交換の場を作り、自衛隊を起点とした人々が活躍できるキャリア形成に関与して参ります。
https://veteranschannel.net/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away