Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Django Invitations & All-Auth: Invitation Accepted Only on Account Creation

django-invitations & django-allauth: Invitation Accepted Only on Account Creation

The Problem

As per this django-invitations issue, django-invitations considers an invite "accepted" if the invite link is clicked. This is problematic for two reasons (or more):

  1. Automatic mail crawlers which visit links checking for malware etc., and verifying that you're not a spammer
  2. A user which clicks a link to "check out" an invite, but doesn't necessarily want to sign up right now

Something to consider is that this solution assumes you're using django-allauth as it uses the EmailAddress model of that package.

Solution

This has been tested, but not extensively.

We take advantage of the fact that Django's url matching magic follows a "first match principle"; the first match that hits is it. So we copy the api of django-invitation's accept url and insert it above our include for django-invitations, pointing to our custom view.

The view is courtesy @clokep (thanks!). It receives the user signed up signal, checks for all matching existing EmailAddresses, accepting the relevant invites. The imports have been added in for clarity.

Thanks to @beekeeper for the project

# this order matters
url(r'^invitations/accept-invite/(?P<key>\w+)/?$', AcceptInviteView.as_view(), name='accept-invite'),
url(r'^invitations/', include('invitations.urls', namespace='invitations')),
# a custom view hooked into a custom url
from django.http import HttpResponse
from django.contrib import messages
from django.shortcuts import redirect
from django.dispatch import receiver
from invitations.views import AcceptInvite
from invitations.app_settings import app_settings
from invitations.adapters import get_invitations_adapter
from invitations.models import Invitation
from invitations.signals import invite_accepted
from allauth.account.signals import user_signed_up
from allauth.account.models import EmailAddress
class AcceptInviteView(AcceptInvite):
def post(self, *args, **kwargs):
self.object = invitation = self.get_object()
# Compatibility with older versions: return an HTTP 410 GONE if there
# is an error. # Error conditions are: no key, expired key or
# previously accepted key.
if app_settings.GONE_ON_ACCEPT_ERROR and \
(not invitation or
(invitation and (invitation.accepted or
invitation.key_expired()))):
return HttpResponse(status=410)
# No invitation was found.
if not invitation:
# Newer behavior: show an error message and redirect.
get_invitations_adapter().add_message(
self.request,
messages.ERROR,
'invitations/messages/invite_invalid.txt')
return redirect(app_settings.LOGIN_REDIRECT)
# The invitation was previously accepted, redirect to the login
# view.
if invitation.accepted:
get_invitations_adapter().add_message(
self.request,
messages.ERROR,
'invitations/messages/invite_already_accepted.txt',
{'email': invitation.email})
# Redirect to login since there's hopefully an account already.
return redirect(app_settings.LOGIN_REDIRECT)
# The key was expired.
if invitation.key_expired():
get_invitations_adapter().add_message(
self.request,
messages.ERROR,
'invitations/messages/invite_expired.txt',
{'email': invitation.email})
# Redirect to sign-up since they might be able to register anyway.
return redirect(app_settings.SIGNUP_REDIRECT)
get_invitations_adapter().stash_verified_email(
self.request, invitation.email)
get_invitations_adapter().add_message(
self.request,
messages.SUCCESS,
'invitations/messages/invite_accepted.txt',
{'email': invitation.email})
return redirect(app_settings.SIGNUP_REDIRECT)
@receiver(user_signed_up)
def accept_invite(sender, request, user, **kwargs):
# Traverse from the user to verified email addresses. It is possible for a
# user to already have multiple email addresses if they typed in a different
# email after accepted the invite. In this case, the user will have two
# emails, but only one will be verified.
addresses = EmailAddress.objects.filter(user=user, verified=True) \
.values_list('email', flat=True)
# Check if any invites exist for this address and were accepted.
invites = Invitation.objects.filter(email__in=addresses)
if invites:
# Mark all these invites as accepted.
invites.update(accepted=True)
for invite in invites:
# Note that this doesn't send it with a request.
invite_accepted.send(sender=Invitation, email=invite.email)
# Figure out if you care if there are multiple invites here.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment