自動化無しに生活無し

WEBとかAIとかLinux関係をひたすら書く備忘録系ブログ

【Django】カスタムユーザーモデルでユーザーブロック機能を実装させる【ManyToManyFieldでユーザーモデル自身を指定】

thumbnail

本格的にSNS等のサービスを運用する時、必要になるのがユーザーフォロー(友達)やユーザーブロックの機能。理論上、いずれもユーザーとユーザーを多対多でつなぐことで実現できる。

今回はユーザーブロックの機能をカスタムユーザーモデルを使って再現する。

コードは【Django】allauthとカスタムユーザーモデルを実装した簡易掲示板を作る【AbstrastBaseUser】から流用している。

ユーザーモデル

users/models.pyの内容、多対多で参照する中間テーブルのモデルクラスが定義されている。時刻を参照しないのであれば、中間テーブルのモデルクラスまで作る必要はない。

from django.db import models

from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin, UserManager
from django.contrib.auth.validators import UnicodeUsernameValidator

from django.utils import timezone

from django.utils.translation import gettext_lazy as _
from django.core.mail import send_mail

import uuid 


#ここ( https://github.com/django/django/blob/master/django/contrib/auth/models.py#L321 )から流用
class CustomUser(AbstractBaseUser, PermissionsMixin):

    username_validator  = UnicodeUsernameValidator()

    id          = models.UUIDField( default=uuid.uuid4, primary_key=True, editable=False )
    username    = models.CharField(
                    _('username'),
                    max_length=150,
                    unique=True,
                    help_text=_('Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.'),
                    validators=[username_validator],
                    error_messages={
                        'unique': _("A user with that username already exists."),
                    },
                )

    first_name  = models.CharField(_('first name'), max_length=150, blank=True)
    last_name   = models.CharField(_('last name'), max_length=150, blank=True)

    email       = models.EmailField(_('email address'))


    #ユーザーのブロック、第一引数"CustomUser"でも構わないが、クラス名が変わると通用しないので、"self"を指定。中間テーブルはBlockUser。
    blocked     = models.ManyToManyField("self",through="BlockUser",through_fields=('to_user', 'from_user'), verbose_name="ブロック",blank=True)


    is_staff    = models.BooleanField(
                    _('staff status'),
                    default=False,
                    help_text=_('Designates whether the user can log into this admin site.'),
                )

    is_active   = models.BooleanField(
                    _('active'),
                    default=True,
                    help_text=_(
                        'Designates whether this user should be treated as active. '
                        'Unselect this instead of deleting accounts.'
                    ),
                )
    date_joined = models.DateTimeField(_('date joined'), default=timezone.now)

    objects     = UserManager()

    EMAIL_FIELD = 'email'
    USERNAME_FIELD = 'username'
    REQUIRED_FIELDS = ['email']

    class Meta:
        verbose_name = _('user')
        verbose_name_plural = _('users')
        #abstract = True

    def clean(self):
        super().clean()
        self.email = self.__class__.objects.normalize_email(self.email)

    def get_full_name(self):
        """
        Return the first_name plus the last_name, with a space in between.
        """
        full_name = '%s %s' % (self.first_name, self.last_name)
        return full_name.strip()

    def get_short_name(self):
        """Return the short name for the user."""
        return self.first_name

    def email_user(self, subject, message, from_email=None, **kwargs):
        """Send an email to this user."""
        send_mail(subject, message, from_email, [self.email], **kwargs)


class BlockUser(models.Model):

    class Meta:
        db_table    = "blockuser"

    #同じクラスを外部キーとして指定しているのでフィールドオプションとしてrelated_nameを指定する。
    id          = models.UUIDField( default=uuid.uuid4, primary_key=True, editable=False )
    dt          = models.DateTimeField(verbose_name="ブロックした日時",default=timezone.now)
    from_user   = models.ForeignKey(CustomUser,verbose_name="ブロック元のユーザー",on_delete=models.CASCADE,related_name="block_from_user")
    to_user     = models.ForeignKey(CustomUser,verbose_name="ブロック対象のユーザー",on_delete=models.CASCADE,related_name="block_to_user")

ManyToManyFieldでCustomUserクラス自身を参照する。そのため、文字列型の"self"と指定する。"CustomUser"でも良いが、それだとクラス名が変わったときに後から修正する必要があるため、この方法が良い。

そして、中間テーブルに当たるモデルクラス、BlockUserでは、2つのフィールドがCustomUserと1対多で繋がっている。2つのフィールドが同じモデルクラスに対して1対多で繋がっているので、フィールドオプションとしてrelated_nameを指定しないといけない。related_nameの指定がないと、マイグレーション時にエラーを起こす。

ユーザーブロック、解除を行う処理

users/views.pyにて全て記述した。アプリ側のbbs/views.pyでも動くがusersアプリ内のmodels.pyを直接インポートすることになるので、あまりよろしくない。ユーザーモデルに対する処理はusersのビューに任せるべきだと思う。

from django.shortcuts import render,redirect
from django.contrib.auth.mixins import LoginRequiredMixin

from .models import BlockUser
from .forms import BlockUserForm

from django.views import View

class UserBlockView(LoginRequiredMixin,View):

    def post(self,request,pk,*args,**kwargs):

        blockusers  = BlockUser.objects.filter(from_user=request.user.id,to_user=pk)

        #すでにある場合は該当レコードを削除、無い場合は挿入
        #TIPS:↑メソッドやビュークラスを切り分けてしまうと、多重に中間テーブルへブロックのレコードが挿入されてしまう可能性があるため1つのメソッド内で分岐するやり方が無難。
        if blockusers:
            print("ある")
            blockusers.delete()

            return redirect("/")

        else:
            print("無い")

        data    = { "from_user":request.user.id,"to_user":pk }
        form    = BlockUserForm(data)

        if form.is_valid():
            print("ブロックOK")
            form.save()

        else:
            print("ブロックNG")

        return redirect("/")


block   = UserBlockView.as_view()

ユーザーモデルを見せびらかす必要はないので、POST文だけでOK。ただし、ブロック解除とブロックの処理はひとつのメソッド内に書き込み、分岐するようにしている。

ブロックと解除の処理をメソッドもしくはビュークラス単位で分けてしまうとブロックの処理を連続で行われる可能性があるからだ。つまり中間テーブルに同じ内容のデータが2個以上スタックされてしまう。

これを許すと中間テーブル内のレコードが増え続けるのでDBの圧迫につながる。そこでビュー側が現状をチェックして、解除するかブロックするか判定している。

ちなみに、BlockUserFormはそのままモデルを継承したフォームクラスである。from_userto_userをfieldsに入れ判定している。

処理後のリダイレクト先にトップページを指定しているが、ここは適宜変えてもらいたい。

テンプレート側の処理

template/bbs/index.htmlより。多対多のユーザーモデルなので、参照の仕方はやや特殊である。

<!DOCTYPE html>
<html lang="ja">
<head>
	<meta charset="UTF-8">
    <meta name="viewport" content="width=device-width,initial-scale=1">
	<title>簡易掲示板</title>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
</head>
<body>

    <main class="container">

        ようこそ、{{ request.user }}


        <form method="POST">
            {% csrf_token %}
            <textarea class="form-control" name="comment"></textarea>
            <input type="submit" value="送信">
        </form>

        {% for topic in topics %}
        <div class="border">
            <div>投稿者 {{ topic.user.last_name }}:{{ topic.user.first_name }}

                {% if request.user.is_authenticated and request.user.id != topic.user.id %}
                <form action="{% url 'users:block' topic.user.id %}" method="POST">
                    {% csrf_token %}
                    <!--ブロック中であればボタン表記を書き換え-->
                    {% if request.user in topic.user.blocked.all %}
                    <button class="btn btn-danger" type="submit">ブロック中</button>
                    {% else %}
                    <button class="btn btn-outline-danger" type="submit">ブロック</button>
                    {% endif %}
                </form>
                {% endif %}
            </div>
            <div>{{ topic.comment }}</div>

            <!--投稿者をブロック中かどうか判定する。レコード2個以上の場合、その数だけ出てしまう。-->
            {% for blocked in topic.user.blocked.all %}
            {% if request.user.id == blocked.id %}<div>この投稿者をブロックしています。</div>{% endif %}
            {% endfor %}

            {% if topic.user.blocked.all %}
            <div>{% for blocked in topic.user.blocked.all %}{{ blocked.last_name }}{{ blocked.first_name }} {% endfor %}
                 がこの投稿者をブロックしています。
            </div>
            {% endif %}

        </div>
        {% endfor %}

    </main>
</body>
</html>

Bootstrapのボタンの装飾を使って、ブロック中とブロックしていない状態で見た目を変えている。

実行するとこうなる。

ブロック

結論

フォローや通報、グループの作成などもこれで表現できそうだ。

ちなみに、下記リンクによると今回のようにManyToManyFieldを含んだモデルクラス自身を多対多の対象にしたい場合、文字列型の"self"を第一引数に指定するのが無難であるとのこと。

https://stackoverflow.com/questions/11721157/django-many-to-many-m2m-relation-to-same-model

ソースコード

https://github.com/seiya0723/django_m2m_block

スポンサーリンク

シェアボタン

Twitter LINEで送る Facebook はてなブログ