[Django] フォームでログイン認証の時と同じようにnext=URLが使えるようにする

Djangoのログイン認証時に?next=URLを付けるとそのURLにリダイレクトさせることができることを知っている人は多いことでしょう。

その機能をそれ以外のFormでも利用できるようにする方法を記しておきます。

ログイン認証時に元いたページへリダイレクトさせる

こちらはご存知の方が多いと思うので簡単に済ませます。

<form action="{% url 'login' %}?next={{ request.path }}" method="post">
    {% csrf_token %}
    {{ form.as_p }}
    <input type="submit" value="Login" />
</form>

<form>action部分に{% url 'login' %}?next={{ request.path }}を書き足すだけで簡単に実現できます。

動的にリダイレクト先を変更できる機能を他のフォームにも付ける

from django.contrib.auth.views import SuccessURLAllowedHostsMixin
from django.utils.http import is_safe_url
from django.views.generic import FormView

REDIRECT_FIELD_NAME = 'next'

class FromNextView(SuccessURLAllowedHostsMixin, FormView):

    redirect_field_name = REDIRECT_FIELD_NAME

    def get_success_url(self):
        url = self.get_redirect_url()
        return url or super().get_success_url()

    def get_redirect_url(self):
        redirect_to = self.request.POST.get(
            self.redirect_field_name,
            self.request.GET.get(self.redirect_field_name, '')
        )
        url_is_safe = is_safe_url(
            url=redirect_to,
            allowed_hosts=self.get_success_url_allowed_hosts(),
            require_https=self.request.is_secure(),
        )
        return redirect_to if url_is_safe else ''

お手軽に実装するためにLoginViewの内容をほぼそのまま真似ました。

あとはこのクラスを通常のFormViewと同様に使うことで?next=URLが利用できるようになります。

利用例

想定できる利用場面としては、特定の条件を満たしていない場合に登録用のフォームに飛ばしてまた元のページに戻す時が考えられます。
例えば、ショッピングサイトなどでクレジットカード情報が未登録の場合に、支払いの前にクレジットカードの登録ページに飛ばしてまた戻ってくる場合など。

class CardRegistrationView(LoginRequiredMixin, FromNextView):
    template_name = 'register.html'
    form_class = CardForm
    success_url = _('card_list')
    
class BuyView(LoginRequiredMixin, FormView):
    template_name = 'buy.html'
    form_class = ItemForm
    success_url = _('bought')
    
    def get(self, request, *args, **kwargs):
        if request.user.card.count == 0:
            """カード情報を登録していない場合カード情報登録ページに飛ばす"""
            response = redirect('card_registration')
            response['location'] += '?next='+request.path
            return response
        return super().get(request, *args, **kwargs)

再利用可能にする

上記の機能を様々なViewに対して利用できるようにMixinクラスとして提供できるようにしてみましょう。

class DynamicRedirectMixin(SuccessURLAllowedHostsMixin):

    redirect_field_name = 'next'

    def get_success_url(self):
        url = self.get_redirect_url()
        return url or super().get_success_url()

    def get_redirect_url(self):
        """Return the user-originating redirect URL if it's safe."""
        redirect_to = self.request.POST.get(
            self.redirect_field_name,
            self.request.GET.get(self.redirect_field_name, '')
        )
        url_is_safe = is_safe_url(
            url=redirect_to,
            allowed_hosts=self.get_success_url_allowed_hosts(),
            require_https=self.request.is_secure(),
        )
        return redirect_to if url_is_safe else ''

こちらとほぼ同様の内容をDjango-Boostというライブラリでも提供しています。

Django-Boost
https://github.com/ChanTsune/django-boost

Django-Boostではテンプレート内で、Python組み込みの関数を利用できるようになったり、ページにアクセス可能な時期を制限したり、モデルに論理削除の機能を追加したり、様々な便利なミックスインクラスを提供しています。

最後に

以上、簡単に割り込みで登録させることができるようになりました。