25
26

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Djangoでいいね!をつくる 2023リファクタリング版

Last updated at Posted at 2020-01-08

はじめに

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

実際のサンプルはリポジトリをみてね

2023年に入ってからサイトをいろいろ改修しててその際に「なんかわかりにくいんだよなー」ってのがきっかけでリファクタリングしようとしたら意外に手こずっちゃったけどいい勉強になった。わかりやすくなったと思う

image.png

検索材料

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

成果物のイメージ

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

処理の流れのイメージ

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

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

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

(appname)/models.py
  :
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

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

ダミーの記事をつくっておく。dbeaverとかmysqlworkbenchなどのクライアントを使って画面上からレコード追加しちゃうほうがかんたん

urls.py

mysite/(appname)/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

mysite/(appname)/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

mysite/(appname)/templates/(appname)/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>
25
26
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
25
26

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?