3-12. ログイン状態に応じて動的に入力フォームを変化させる

今回のテーマは「ログイン状態に応じて動的に入力フォームを変化させる」です。せっかくログイン機能を付与したので、ログインユーザーは名前やメールアドレスを入力しなくても投稿出来るように、しましょう。また、未ログインユーザーはゲストとしてメールアドレスと名前を入力すればスレッド作成やコメント投稿が出来るように修正していきましょう。

※本ページは「3-11. 認証バックエンドをカスタマイズしてログイン方法を変更する」まで読まれた方を対象としています。そのためサンプルソースコードが省略されている場合があります。


モデルの修正

ログインユーザーとTopicモデル、Commentモデルを結びつけましょう。また、ゲストユーザー用にメールアドレスも追加しましょう。

thread/forms.py(一部抜粋)


class TopicModelForm(forms.ModelForm):
    # ゲストユーザー用入力項目
    user_name = forms.CharField(
        label='ゲストユーザー名',
        required=True,
    )
    email = forms.EmailField(
        label='メールアドレス',
        required=True,
    )

    class Meta:
        model=Topic
        fields=[
            'title',
            # 'user_name',
            # 'email',
            'category',
            'message',
        ]
        widgets = {
            'title' : forms.TextInput(attrs={'class': 'hoge'}),
            # 'user_name' : forms.TextInput(attrs={'value': '名無し'}),
        }

    def __init__(self, *args, **kwargs):
        kwargs.setdefault('label_suffix', '')
        super().__init__(*args, **kwargs)
        self.fields['category'].empty_label = '選択して下さい'
        self.fields['user_name'].widget.attrs['value'] = 'anonymous'

    def save(self, commit=True):
        topic = super().save(commit=False)
        topic.user_name = self.cleaned_data['user_name']
        topic.email = self.cleaned_data['email']
        if commit:
            topic.save()
        return topic

        # self.fields['user_name'].widget.attrs['value'] = '匿名'

ゲストユーザー用のスレッド作成フォームの作成

ゲストユーザーがスレッドを作成出来るようにTopicModelFormを改修しましょう。thread/forms.pyを以下のように修正します。

thread/forms.py(一部抜粋)


class TopicModelForm(forms.ModelForm):
    # ゲストユーザー用入力項目
    user_name = forms.CharField(
        label='ゲストユーザー名',
        required=True,
    )
    email = forms.EmailField(
        label='メールアドレス',
        required=True,
    )

    class Meta:
        model=Topic
        fields=[
            'title',
            # 'user_name',
            # 'email',
            'category',
            'message',
        ]
        widgets = {
            'title' : forms.TextInput(attrs={'class': 'hoge'}),
            # 'user_name' : forms.TextInput(attrs={'value': '名無し'}),
        }

    def __init__(self, *args, **kwargs):
        kwargs.setdefault('label_suffix', '')
        super().__init__(*args, **kwargs)
        self.fields['category'].empty_label = '選択して下さい'
        self.fields['user_name'].widget.attrs['value'] = 'anonymous'

    def save(self, commit=True):
        topic = super().save(commit=False)
        topic.user_name = self.cleaned_data['user_name']
        topic.email = self.cleaned_data['email']
        if commit:
            topic.save()
        return topic

        # self.fields['user_name'].widget.attrs['value'] = '匿名'

敢えて、user_nameとemailをクラス変数として追加しています。これはuser_nameとemailについては管理画面では任意項目として、ゲストユーザーの入力フォームのみで必須項目としたいためです。管理画面においても必須項目とする場合にはMetaクラス内のfieldsで指定しても構わないと思います。

ログインユーザー用のスレッド作成フォームの作成

次にログインユーザーが入力するフォームを作成していきます。thread/forms.pyに以下のように追記します。

thread/forms.py(一部抜粋)


class LoginedUserTopicModelForm(forms.ModelForm):
    class Meta:
        model=Topic
        fields=[
            'title',
            'category',
            'message',
        ]
        widgets = {
            'title' : forms.TextInput(attrs={'class': 'hoge'}),
        }

    def __init__(self, *args, **kwargs):
        kwargs.setdefault('label_suffix', '')
        super().__init__(*args, **kwargs)
        self.fields['category'].empty_label = '選択して下さい'
        # self.fields['user_name'].widget.attrs['value'] = '匿名'
    
    def save(self, user, commit=True, **kwargs):
        topic = super().save(commit=False)
        topic.user = user
        if commit:
            topic.save()
        return topic

特に解説は不要かと思います。save関数はオーバーライドしてカスタムユーザーを指定して保存するようにしています。

ゲストユーザー用のコメント投稿フォームの作成

次にゲストユーザーがコメント投稿するためのフォームを用意するためにCommentModelFormを改修します。thread/forms.pyを修正します。

thread/forms.py(一部抜粋)


class CommentModelForm(forms.ModelForm):
    # ゲストユーザー用入力項目
    user_name = forms.CharField(
        label='お名前',
        required=True,
    )
    email = forms.EmailField(
        label='メールアドレス',
        required=True,
    )

    class Meta:
        model = Comment
        fields = [
            'message',
            'image',
        ]

    def __init__(self, *args, **kwargs):
        kwargs.setdefault('label_suffix', '')
        super().__init__(*args, **kwargs)
        self.fields['user_name'].widget.attrs['value'] = 'anonymous'

    def save_with_topic(self, topic_id, commit=True, **kwargs):
        comment = self.save(commit=False)
        comment.topic = Topic.objects.get(id=topic_id)
        comment.no = Comment.objects.filter(topic_id=topic_id).count() + 1
        comment.user_name = self.cleaned_data['user_name']
        comment.email = self.cleaned_data['email']
        if commit:
            comment.save()
        return comment

改修方針はTopicModelFormと同じですね。save_with_topic関数でもuser_nameとemailをUserオブジェクトに渡していますので忘れないようにして下さい。

ログインユーザー用のコメント作成フォームの作成

フォームの準備の最後はログインユーザーがコメント投稿するためのフォームの準備です。thread/forms.pyに追記していきます。

thread/forms.py(一部抜粋)


class LoginedUserCommentModelForm(forms.ModelForm):
    class Meta:
        model = Comment
        fields = [
            'message',
            'image',
        ]

    def __init__(self, *args, **kwargs):
        kwargs.setdefault('label_suffix', '')
        super().__init__(*args, **kwargs)

    def save_with_topic(self, topic_id, user, commit=True, **kwargs):
        comment = self.save(commit=False)
        comment.topic = Topic.objects.get(id=topic_id)
        comment.no = Comment.objects.filter(topic_id=topic_id).count() + 1
        comment.user = user
        if commit:
            comment.save()
        return comment

特に難しい部分はないと思います。save_with_topic関数には仮引数にuserがあるので注意して下さい。

ログイン状態に応じてフォームを変更するようにビューを修正する

さて、ここまで準備したフォームをログイン状態によって動的に使い分けていきます。ゲストユーザーの状態(未ログイン状態)ではCommentModelFormとTopicModelFormを使用します。そしてログイン状態ではLoginedUserCommentModelFormとLoginedUserTopicModelFormを使用します。この切り替えを動的に行うためにビューの中でget_form_class関数をオーバライドします。ではthread/views.pyの改修を見ていきましょう。

thread/views.py(一部抜粋)


#importはファイル文頭で
from . forms import (
    TopicModelForm, TopicForm, CommentModelForm,
    LoginedUserTopicModelForm, LoginedUserCommentModelForm
)
from . models import Topic, Category, Comment


class TopicCreateView(CreateView):
    template_name = 'thread/create_topic.html'
    form_class = LoginedUserTopicModelForm
    model = Topic
    success_url = reverse_lazy('base:top')

    def get_form_class(self):
        '''
        ログイン状態によってフォームを動的に変更する
        '''
        if self.request.user.is_authenticated:
            return LoginedUserTopicModelForm
        else:
            return TopicModelForm

    def form_valid(self, form):
        ctx = {'form': form}
        if self.request.POST.get('next', '') == 'confirm':
            ctx['category'] = form.cleaned_data['category']
            return render(self.request, 'thread/confirm_topic.html', ctx)
        elif self.request.POST.get('next', '') == 'back':
            return render(self.request, 'thread/create_topic.html', ctx)
        elif self.request.POST.get('next', '') == 'create':
            form.save(self.request.user)
            # メール送信処理
            template = get_template('thread/mail/topic_mail.html')
            user_name = self.request.user.username if self.request.user else form.cleaned_data['user_name']
            mail_ctx={
                'title': form.cleaned_data['title'],
                'user_name': user_name,
                'message': form.cleaned_data['message'],
            }
            EmailMessage(
                subject='トピック作成: ' + form.cleaned_data['title'],
                body=template.render(mail_ctx),
                from_email='hogehoge@example.com',
                to=['admin@example.com'],
                cc=['admin2@example.com'],
                bcc=['admin3@example.com'],
            ).send()
            # return super().form_valid(form)
            return redirect(self.success_url)
        else:
            # 正常動作ではここは通らない。エラーページへの遷移でも良い
            return redirect(reverse_lazy('base:top'))

class TopicAndCommentView(FormView):
    template_name = 'thread/detail_topic.html'
    form_class = LoginedUserCommentModelForm

    def get_form_class(self):
        '''
        ログイン状態によってフォームを動的に変更する
        '''
        if self.request.user.is_authenticated:
            return LoginedUserCommentModelForm
        else:
            return CommentModelForm
    
    def form_valid(self, form):
        # comment = form.save(commit=False)
        # comment.topic = Topic.objects.get(id=self.kwargs['pk'])
        # comment.no = Comment.objects.filter(topic=self.kwargs['pk']).count() + 1
        # comment.save()

        # Comment.objects.create_comment(
        #     user_name=form.cleaned_data['user_name'],
        #     message=form.cleaned_data['message'],
        #     topic_id=self.kwargs['pk'],
        #     image=form.cleaned_data['image']
        # )
        kwargs = {}
        if self.request.user.is_authenticated:
            kwargs['user'] = self.request.user

        form.save_with_topic(self.kwargs.get('pk'), **kwargs)
        response = super().form_valid(form)
        return response

    def get_success_url(self):
        return reverse_lazy('thread:topic', kwargs={'pk': self.kwargs['pk']})
    
    def get_context_data(self):
        ctx = super().get_context_data()
        ctx['topic'] = Topic.objects.get(id=self.kwargs['pk'])
        ctx['comment_list'] = Comment.objects.filter(
                topic_id=self.kwargs['pk']).annotate(vote_count=Count('vote')).order_by('no')
        return ctx

ポイントはrequest.user.is_authenticated()メソッドによって使用するフォームを使い分けているところですね。TopicオブジェクトやCommentオブジェクトの保存処理は各フォームオブジェクトの同一メソッドを呼び出すことで共通化しています。

念のため、上記のビュークラスを使用するようにURLも確認しておきましょう。

thread/urls.py


from django.urls import path

from . import views
app_name = 'thread'


urlpatterns = [
    # path('create_topic/', views.TocicCreateViewBySession.as_view(), name='create_topic'),
    path('create_topic/', views.TopicCreateView.as_view(), name='create_topic'),
    # path('create_topic/', views.topic_create, name='create_topic'),
    path('/', views.TopicAndCommentView.as_view(), name='topic'),
    # path('category//', views.CategoryView.as_view(), name='category'),
    path('category//', views.show_catgegory, name='category'),
]

テンプレートの修正

ログインしている場合とゲストユーザーの場合で表示を切り替えるため、テンプレートにも修正を加えましょう。テンプレートにはuserが渡されていますので、このuserで判断をしていきます。

スレッド作成画面
templates/thread/create_topic.html.py



{% extends 'base/base.html' %}
{% block title %}スレッド作成 - {{ block.super }}{% endblock %}
{% block content %}
<div class="ui grid stackable">
    <div class="eleven wide column">
        <div class="ui breadcrumb">
            <a href="{% url 'base:top' %}" class="section">TOP</a>
            <i class="right angle icon divider"></i>
            <a class="active section">スレッド作成</a>
        </div>
        <div class="ui segment">
            <div class="content">
                <div class="header"><h3>スレッド作成</h3></div>
                {% if not user.is_authenticated %}
                <div class="ui warning message">
                    <p>ゲストユーザーでスレッド作成しますか?ログインする場合は以下のボタンでログインして下さい。</p>
                    <a class="ui button orange" href="/accounts/login?next=/thread/create_topic/">ログイン</a>
                </div>
                {% endif %}
                <form class="ui form" action="{% url 'thread:create_topic' %}" method="POST">
                    {% csrf_token %}
                    <!-- {{form.as_p}} -->
                    {% for field in form %}
                    <div class="field">{{field.label_tag}}{{field}}</div>
                        {% for error in field.errors%}
                        <p style="color: red;">{{error}}</p>
                        {% endfor%}
                    {% endfor %}
                    <button type="submit" class="ui button" name="next" value="confirm">作成</button>
                </form>
            </div>
        </div>
    </div>
    {% include 'base/sidebar.html' %}
</div>
{% endblock %}

templates/thread/confirm_topic.html.py



{% extends 'base/base.html' %}
{% block title %}スレッド作成 - {{ block.super }}{% endblock %}
{% block content %}
<div class="ui grid stackable">
    <div class="eleven wide column">
        <div class="ui breadcrumb">
            <a href="{% url 'base:top' %}" class="section">TOP</a>
            <i class="right angle icon divider"></i>
            <a class="active section">スレッド作成</a>
        </div>
        <div class="ui segment">
            <div class="content">
                <div class="header"><h3>スレッド作成</h3></div>
                <p>内容を確認してください</p>
                <table  class="ui celled table table table-hover" >
                    <tr><td>タイトル</td><td>{{form.title.value}}</td></tr>
                    {% if user.is_authenticated %}
                    <tr><td>お名前</td><td>{{user.username}}</td></tr>
                    {% else %}
                    <tr><td>ゲスト名</td><td>{{form.cleaned_data.user_name}}</td></tr>
                    {% endif %}
                    <tr><td>カテゴリー</td><td>{{form.cleaned_data.category}}</td></tr>
                    <tr><td>本文</td><td><pre>{{form.message.value}}</pre></td></tr>
                </table>
                <form class="ui form" action="{% url 'thread:create_topic' %}" method="POST">
                    {% csrf_token %}
                    {% for field in form %}
                        {{field.as_hidden}}
                    {% endfor %}
                    <button class="ui button grey" type="submit" name="next" value="back">戻る</button>
                    <button class="ui button orange" type="submit" name="next" value="create">作成</button>
                </form>
            </div>
        </div>
    </div>
    {% include 'base/sidebar.html' %}
</div>
{% endblock %}

templates/thread/detail_topic.html



{% extends 'base/base.html' %}
{% block title %}トピック作成 - {{ block.super }}{% endblock %}
{% block content %}
{% load threadfilters %}
{% load static %}
<div class="ui grid stackable">
    <div class="eleven wide column">
        <div class="ui breadcrumb">
            <a href="{% url 'base:top' %}" class="section">TOP</a>
            <i class="right angle icon divider"></i>
            <a href="{% url 'thread:category' url_code=topic.category.url_code %}" class="section">{{topic.category.name}}</a>
            <i class="right angle icon divider"></i>
            <a class="active section">{{topic.title}}</a>
        </div>
        <div class="ui segment">
            <div class="content">
                <div class="header"><h3>{{topic.title}}</h3></div>
                <p>{% if topic.user %}{{topic.user}}{% else %}{{topic.user_name}}(Guest){% endif %} - {{topic.created}}</p>
                <div class="ui segment">
                    <p><pre>{{topic.message}}</pre></p>
                </div>
            </div>
        </div>
        <!--コメント表示-->
        <div class="ui segment">
            {% if comment_list %}
            {% for comment in comment_list %}
            <div class="ui segment secondary">
                <p>{{comment.no}}. {% if comment.user %}{{comment.user.username}}{% else %}{{comment.user_name}}(Guest){% endif %}
                <br>{{comment.created}}</p>
                {% if comment.pub_flg %}
                <p>{{comment.message | comment_filter | safe}}</p>
                    {% if comment.image %}
                    <a href="{{comment.image.url}}" target="_blank" rel="noopener noreferrer"><img decoding="async" src="{{comment.image.url}}" width=200px></a>
                    {% endif %}
                <div class="ui right aligned vertical segment">
                    <div class="vote_button" style="cursor: pointer;"
                        data-comment-id="{{comment.id}}" data-count="{{comment.vote_count}}">
                        <i class="heart outline icon"></i>
                        <span class="vote_counter">
                            {% if comment.vote_count > 0 %}{{comment.vote_count}}{% endif %}
                        </span>
                    </div>
                </div>
                {% else %}
                <p style="color: grey">コメントは非表示とされました</p>
                {% endif %}
            </div>
            {% endfor %}
            {% else %}
            <div class="ui warning message"><p>まだコメントはありません</p></div>
            {% endif %}
        </div>
        <!--//コメント表示-->
        <!--コメント投稿-->
        <h4>コメント投稿</h4>
        {% if not user.is_authenticated %}
        <div class="ui warning message">
            <p>ゲストユーザーで投稿しますか?ログインする場合は以下のボタンでログインして下さい。</p>
            <a class="ui button orange" href="/accounts/login?next=/thread/{{topic.id}}/">ログイン</a>
        </div>
        {% endif %}
        <div class="ui segment">
            <form class="ui form" action="" method="POST" enctype="multipart/form-data">
                {% csrf_token %}
                {{form.as_p}}
                <button class="ui button orange" type="submit">コメント投稿</button>
            </form>
        </div>
        <!--//コメント投稿-->
    </div>
    {% include 'base/sidebar.html' %}
</div>
{% endblock %}
{% block js %}
<script src="{% static 'js/vote.js' %}" type='text/javascript'></script>
{% endblock %}

基本的にはuser.is_authenticatedで場合分けしていくことでログインしている場合としていない場合を区別することが出来ます。

動作確認

では、動作確認をしてみましょう。

まずはゲストユーザーでスレッド作成をしてみましょう。

ゲストユーザーでのコメント作成も出来ますね。

次にログインユーザーでスレッド作成してみましょう。

ログインユーザーでのコメント投稿画面です。

最後に

Sponsored Link


コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です