Python 如何使用django allauth实现用于注册/登录的邀请流?

Python 如何使用django allauth实现用于注册/登录的邀请流?,python,django,django-allauth,Python,Django,Django Allauth,背景 我正在开发一个应用程序,用户可以邀请其他人在不同的资源上进行协作。被邀请的人可能已经是该应用程序的用户,也可能是该应用程序的全新用户。当我使用allauth进行注册/登录时,受邀者可以通过标准注册/登录表单或通过三个社交帐户之一(fb、twitter、谷歌)响应邀请 由于这些要求,如果现有用户接受邀请,子类化DefaultAccountAdapter并覆盖is\u open\u for\u signup方法将不起作用,因为这不是登录流程的一部分 流量 用户提交邀请表单,指定收件人的电子

背景

我正在开发一个应用程序,用户可以邀请其他人在不同的资源上进行协作。被邀请的人可能已经是该应用程序的用户,也可能是该应用程序的全新用户。当我使用allauth进行注册/登录时,受邀者可以通过标准注册/登录表单或通过三个社交帐户之一(fb、twitter、谷歌)响应邀请

由于这些要求,如果现有用户接受邀请,子类化
DefaultAccountAdapter
并覆盖
is\u open\u for\u signup
方法将不起作用,因为这不是登录流程的一部分


流量

  • 用户提交邀请表单,指定收件人的电子邮件地址
  • 已发送邀请电子邮件,其中包含邀请接受表单的链接
  • 用户点击链接到接受表单-他们可能已经或可能还没有自己的应用程序用户帐户
  • 由于邀请接受链接包含此邀请的唯一密钥,因此视图会将“邀请密钥”添加到会话中
  • 被邀请者可以选择注册或登录现有用户帐户以接受邀请
  • 被邀请人完成注册/登录后,将收到“用户已注册”或“用户已登录”信号,并检查会话是否有“邀请密钥”,以确认新用户刚刚接受了邀请
  • 使用键检索invite,并针对新用户处理invite

逻辑

验收视图的url模式

url(r'^invitation/(?P<invite_key>[\w\d]+)/$', views.ResourceInviteAcceptanceView.as_view(), name='resource-invite-accept'),
from django.contrib.auth.models import User
from django.shortcuts import get_object_or_404
from django.dispatch import receiver

from allauth.account import app_settings
from allauth.account.forms import LoginForm, SignupForm
from allauth.account.utils import get_next_redirect_url, complete_signup
from allauth.account.signals import user_signed_up, user_logged_in

from forms.views import MultiFormsView
from api.models import ResourceInvite

class ResourceInviteAcceptanceView(MultiFormsView):
    template_name = 'public/resource_invite_accept.html'
    form_classes = {'login': LoginForm,
                    'signup': SignupForm}
    redirect_field_name = "next"

    def get_invite(self):
        invite_key = self.kwargs['invite_key']
        invite = get_object_or_404(ResourceInvite, key=invite_key)
        return invite

    def get_login_initial(self):
        invite = self.get_invite()
        return {'login':invite.email}

    def get_signup_initial(self):
        invite = self.get_invite()
        return {'email':invite.email}

    def get_context_data(self, **kwargs):
        context = super(ResourceInviteAcceptanceView, self).get_context_data(**kwargs)
        context.update({"redirect_field_name": self.redirect_field_name,
                        "redirect_field_value": self.request.REQUEST.get(self.redirect_field_name)})
        return context

    def get_success_url(self):
        # Explicitly passed ?next= URL takes precedence
        ret = (get_next_redirect_url(self.request,
                                     self.redirect_field_name)
               or self.success_url)
        return ret

    def login_form_valid(self, form):
        return form.login(self.request, redirect_url=self.get_success_url())

    def signup_form_valid(self, form):
        user = form.save(self.request)
        return complete_signup(self.request, user,
                               app_settings.EMAIL_VERIFICATION,
                               self.get_success_url())

    def get(self, request, *args, **kwargs):
        session = request.session
        session['invite_key'] = self.kwargs['invite_key'] 
        return super(ResourceInviteAcceptanceView, self).get(request, *args, **kwargs)


@receiver ([user_signed_up, user_logged_in], sender=User)
def check_for_invite(sender, **kwargs):
    signal = kwargs.get('signal', None)
    user = kwargs.get('user', None)
    request = kwargs.get('request', None)
    session = request.session
    invite_key = session.get('invite_key')
    if invite_key:
        invite = get_object_or_404(ResourceInvite, key=invite_key)
        """ logic to process invite goes here """
        del session['invite_key']

问题

只要被邀请者点击链接并完成邀请接受过程,这一切都可以正常工作

但是

如果他们在该过程中的任何时候(明确地或由于错误)退出,“invite_key”仍然存在于会话中,因此在下一个人(他们或其他人)注册或登录时得到处理


问题

处理这个问题的最好办法是什么?是否有其他点可以将“invite_key”添加到会话中,从而保证用户已经实际接受了邀请

对于标准注册/登录,这可能是一个覆盖的“forms\u valid”方法,因为我们现在知道,用户已经完成了这些过程中的任何一个。但我不知道当他们使用社交注册/sigin时,在哪里/如何添加“invite_key”


--更新--

可能的解决方案#1

通过社交登录,向会话添加邀请密钥的最佳位置(确保用户正在通过社交登录接受邀请)似乎是通过向“pre_social_login”信号添加接收者。我遇到的问题是,如何确保在触发信号时仍然可以访问密钥,以便将其添加到会话中

一个失败的解决方案是在receiver函数中访问HTTP_REFERER,该函数可以包含邀请url。可以从该会话中删除密钥,然后将其添加到会话中。但如果用户是该应用程序的新用户或当前未登录到其社交帐户,则此操作将失败,因为他们首先被重定向到社交帐户登录页面(在社交帐户域上),然后当发生回调重定向并触发信号时,HTTP_REFERER的值将不再存在


我无法找到一个好方法,使邀请键值可以在信号接收器函数中访问,如果没有它,将导致相同的原始问题?

我已经想出了一个解决方案,但我对它不是100%满意,因为它涉及到对
allauth.socialaccount.models.SocialLogin
上的
请求
类方法中的
状态进行修补

原因如下:

  • 这是所有提供者在启动社交身份验证过程时调用的单一共享逻辑点

  • 社交登录过程中,
    SocialLogin
    的“状态”属性已存储在会话中,然后在完成过程中检索并通过“pre_social_login”信号传递

这是从请求中检索特定值的原始方法,然后将这些值存储在会话中,以便在流程完成后供allauth使用

@classmethod
def state_from_request(cls, request):
    state = {}
    next_url = get_next_redirect_url(request)
    if next_url:
        state['next'] = next_url
    state['process'] = request.REQUEST.get('process', 'login')
    return state
这是补丁

def state_from_request_wrapper(wrapped_func):
    wrapped_func = wrapped_func.__func__
    def _w(cls, request):
        state = wrapped_func(cls, request)
        invite_key = extract_invite_key(request)
        if invite_key:
            state['invite_key'] = invite_key
        return state
    return classmethod(_w)

def extract_invitation_key(request):
    referer = request.META.get('HTTP_REFERER')
    if not referer:
        return None
    p = re.compile('iv/(?P<invite_key>[\w\d]+)/$')
    match = p.search(referer)
    if not match:
        return None
    return match.group(1)

SocialLogin.state_from_request = state_from_request_wrapper(SocialLogin.state_from_request)
这些是信号接收器的功能

@receiver (pre_social_login, sender=SocialLogin)    
def check_pre_social_login(sender, **kwargs):
    social_login = kwargs['sociallogin']
    request = kwargs['request']
    session = request.session
    invite_key = social_login.state.get('invite_key')
    if invite_key:
        session['invite_key'] = invite_key


@receiver ([user_signed_up, user_logged_in], sender=User)
def check_for_invite(sender, **kwargs): 
    request = kwargs['request']
    session = request.session
    invite_key = session.get('invite_key')
    if invite_key:
        invite = get_object_or_404(ResourceInvite, key=invite_key)
        process_invite(kwargs['user'], invite, True)
        del session['invite_key']


def process_invite(user, invite, accept):
    ...
    # process invite here

与此同时,有人刚刚构建了一个python包(django邀请函),它可以很好地完成这项工作,而且不需要进行修补


在Github上查看它:

据我所知,
django邀请
并不能解决我描述的用例。如果您阅读了背景资料,我会给出原因---
由于这些要求,如果现有用户接受邀请,子类化DefaultAccountAdapter并重写is_open_for_注册方法将不起作用,因为这不是登录流的一部分。
@receiver (pre_social_login, sender=SocialLogin)    
def check_pre_social_login(sender, **kwargs):
    social_login = kwargs['sociallogin']
    request = kwargs['request']
    session = request.session
    invite_key = social_login.state.get('invite_key')
    if invite_key:
        session['invite_key'] = invite_key


@receiver ([user_signed_up, user_logged_in], sender=User)
def check_for_invite(sender, **kwargs): 
    request = kwargs['request']
    session = request.session
    invite_key = session.get('invite_key')
    if invite_key:
        invite = get_object_or_404(ResourceInvite, key=invite_key)
        process_invite(kwargs['user'], invite, True)
        del session['invite_key']


def process_invite(user, invite, accept):
    ...
    # process invite here