cradmin_invite — A generalized invite workflow

The purpose of the django_cradmin.apps.cradmin_invite app is to provide a general purpose invite workflow.

Install

Add the following to INSTALLED_APPS:

INSTALLED_APPS = (
    # ...
    'django_cradmin',
    'django_cradmin.apps.cradmin_generic_token_with_metadata',
    'django_cradmin.apps.cradmin_invite',
)

Set the DJANGO_CRADMIN_SITENAME setting:

DJANGO_CRADMIN_SITENAME = 'Testsite'

Tutorial

In this tutorial we will create a workflow/process that can be used to invite a user to become administrator for a Site. Our example assumes you have a Django app named myapp.

The Site model

Lets say we have the following model:

from django.db import models
from django.conf import settings

class Site(models.Model):
    name = models.CharField(max_length=255)
    admins = models.ManyToManyField(settings.AUTH_USER_MODEL)

Create the invite email

Create a subclass of django_cradmin.apps.cradmin_invite.invite_url.InviteUrl:

from django_cradmin.apps.cradmin_invite.invite_url import InviteUrl

class SiteAdminInviteUrl(InviteUrl):
    def get_appname(self):
        return 'myapp'

    def get_confirm_invite_url(self, generictoken):
        # URL of the AcceptSiteAdminInviteView shown below
        return reverse('siteadmin-invite-accept', kwargs={
            'token': generictoken.token
        })

And use it to send the invite email to test@example.com:

from django.views.generic import View
from myapp.models import Site

class CreateInviteView(View):
    def get(self, request):
        # NOTE: You will most likely want to put this code in a post() method
        #       and use a form as input.
        site = Site.objects.first()  # Code to get at Site object
        invite = SiteAdminInviteUrl(request=request, private=True, content_object=site)
        invite.send_email('test@example.com')

Notice that we add the ID of the site as metadata. We need this to know which site to add the user accepting the invite to.

Create the view responsible for adding the user as admin

Create a subclass of django_cradmin.apps.cradmin_invite.baseviews.AbstractAcceptInviteView:

from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404
from django.conf import settings

from django_cradmin.apps.cradmin_invite.baseviews.accept import AbstractAcceptInviteView
from myapp.models import Site

class AcceptSiteAdminInviteView(AbstractAcceptInviteView):
    description_template_name = 'myapp/invite_description.django.html'

    def get_appname(self):
        return 'myapp'

    def invite_accepted(self, generictoken):
        site = generictoken.content_object
        site.admins.add(self.request.user)
        messages.success(self.request, 'You are now admin on %(site)s' % {'site': site})
        return HttpResponseRedirect(settings.LOGIN_URL)

Add to urls

Add the views to your url patterns:

urlpatterns = patterns(
    # ...
    url(r'^siteadmin/invite/create$',
        CreateInviteView.as_view(),
        name="siteadmin-invite-accept"),
    url(r'^siteadmin/invite/accept/(?P<token>.+)$',
        AcceptSiteAdminInviteView.as_view(),
        name="siteadmin-invite-accept"),
    # ...
)

Private vs public InviteUrl

See get_share_url() and send_email().

The InviteUrl class

class InviteUrl(request, private, content_object, metadata=None, **kwargs)

Bases: object

Sends an email with a link that the user clicks to accept an invite.

Example

from django_cradmin.apps.cradmin_invite.utils import InviteUrl

class InviteUrl(InviteUrl):
    def get_appname(self):
        return 'myapp'

    def get_confirm_invite_url(self, generictoken):
        return reverse('myapp-confirm-invite', kwargs={
            'token': generictoken.token
        })

def myview(request):
    InviteUrl(request=request, private=True, content_object=someobject).send_email(
        'test@example.com', 'test2@example.com')
    # ... or ...
    share_url = InviteUrl(request=request, private=False, content_object=someobject).get_share_url()
    # ... or ...
    InviteUrl(request=request, private=False, content_object=someobject).send_email(
        'test@example.com', 'test2@example.com', 'test3@example.com')
Parameters:
  • request – A Django HttpRequest object.
  • private – If this is True we send unique single-use invite URLs.
  • metadata – Metadata to accociate with the invite.
get_appname()

Get the appname for django_cradmin.apps.cradmin_generic_token_with_metadata.models.GenericTokenWithMetadata.app.

You must override this in subclasses.

get_confirm_invite_url(generictoken)

Get the confirm invite view URL.

Must be implemented in subclasses.

Parameters:generictoken – A GenericTokenWithMetadata object.
get_from_email()

Get the email sender address.

Defaults to the DJANGO_CRADMIN_INVITE_FROM_EMAIL setting falling back on the DEFAULT_FROM_EMAIL setting.

get_expiration_datetime()

Get the value to use for the expiration_datetime attribute of GenericTokenWithMetadata.

Defaults to the expiration_datetime provided via the constructor, and falls back to getting the configured expiration datetime for the app.

get_invite_email_class()

Must return a subclass of django_cradmin.apps.cradmin_email.emailutils.AbstractEmail.

Defaults to InviteEmail.

get_extra_invite_email_context_data(generictoken)

Override this to provide extra context data for the get_invite_email_class().

Make sure you call super and extend the returned dict.

send_email(*emails)

Generate a token and send an email containing an absolute invite URL for that token.

If this InviteUrl is private, we generate a new token each email recipient, and if it is public, we re-use the same token.

Private tokens are generated as single use tokens, and public tokens are unlimited use tokens.

Private tokens gets the email automatically added to the metadata if metadata is a dict or None.

get_share_url()

Generate a token and return an absolute invite URL for that token.

If this InviteUrl is private, we generate a new token each time this is called, and if it is public, we re-use the same token.

Private tokens are generated as single use tokens, and public tokens are unlimited use tokens.

Email templates and how to override them

You can override the following templates:

cradmin_invite/email/subject.django.txt
Override this to set the email subject.
cradmin_invite/email/message.django.txt
Override this to change the email message.

All of the email templates get the following context variables:

  • DJANGO_CRADMIN_SITENAME: The value of the setting with the same name.
  • activate_url: The URL that users should click to activate their account.

UI messages/labels and how to override them

You do not have to override the entire template to adjust the text in the AbstractAcceptInviteView UI. We provide the following class methods for you to override:

get_pagetitle() Get the title of the page.
get_description_template_name() The template used to render the description of the invite.
get_accept_as_button_label() Get the label of the Accept as authenticated user button.
get_register_account_button_label() Get the label of the Sign up button.
get_login_as_different_user_button_label() Get the label of the Sign in as different user button.
get_login_button_label() Get the label of the Sign in button.

The token error template

When the token fails to validate because it has expired or because the user does not copy the entire URL into their browser, we respond with token_error_response(). You normally do not want to override this method, but instead override token_error_template_name or get_token_error_template_name().

Instead of creating a completely custom template, you can extend cradmin_invite/accept/token_error.django.html and just override the invalid_token_message and expired_token_message blocks:

{% extend "cradmin_invite/accept/token_error.django.html" %}
{% load i18n %}

{% block invalid_token_message %}
    {% trans "Invalid invite URL. Are you sure you copied the entire URL from the email?" %}
{% endblock invalid_token_message %}

{% block expired_token_message %}
    {% trans "This invite link has expired." %}
{% endblock expired_token_message %}

The AbstractAcceptInviteView class

class AbstractAcceptInviteView(**kwargs)

Bases: django.views.generic.base.TemplateView, django_cradmin.javascriptregistry.viewmixin.StandaloneBaseViewMixin

Base class for views that accept invites from InviteUrl.

You have to override:

Constructor. Called in the URLconf; can contain helpful extra keyword arguments, and other things.

description_template_name = 'cradmin_invite/accept/description.django.html'

Default value for get_description_template_name().

token_error_template_name = 'cradmin_invite/accept/token_error.django.html'

Default value for get_token_error_template_name().

get_pagetitle()

Get the title of the page.

get_accept_as_button_label()

Get the label of the Accept as authenticated user button.

get_register_account_button_label()

Get the label of the Sign up button.

get_login_as_different_user_button_label()

Get the label of the Sign in as different user button.

get_login_button_label()

Get the label of the Sign in button.

get_description_template_name()

The template used to render the description of the invite. The template context is the one returned by get_context_data().

Defaults to description_template_name.

get_token_error_template_name()

The template used to render the view when the given token does not validate. If you just want to change the error messages, you can extend the cradmin_invite/accept/token_error.django.html template and override the invalid_token_message and expired_token_message blocks.

Defaults to token_error_template_name.

dispatch(request, *args, **kwargs)

Takes care of validating the token for both GET and POST requests.

If the token is valid, we set self.generic_token to the GenericTokenWithMetadata.

If the token is invalid, we respond with token_error_response().

token_error_response(token_does_not_exist=False, token_expired=False)

Generates the response when the token does not validate.

Unless you have some special needs, you will most likely want to just override token_error_template_name instead of this view.

add_next_argument_to_url(url, next_url=None)

Adds ?next=<next_url> to the given url.

Parameters:
  • url – The base url.
  • next_url – The url to redirect to after whatever url does is successfully completed. Defaults to the absolute URI of the current request.
get_register_account_url()

Get the URL to used to create a new user.

Should have some way of returning the user to this view after the user has been created.

Defaults to the cradmin-register-account view in django_cradmin.apps.cradmin_register_account.

get_login_url()

Get the URL to used to login if not already authenticated.

Should have some way of returning the user to this view after login is complete.

Defaults to settings.LOGIN_URL.

get_login_as_different_user_url()

Get the URL to used to login as a different user.

Should have some way of returning the user to this view after login is complete.

Defaults to the cradmin-authenticate-logout view in django_cradmin.apps.cradmin_authenticate with the next-argument set to the cradmin-authenticate-login view in the same app, with the next argument of the cradmin-authenticate-login view set to the current url.

post(*args, **kwargs)

If the user is authenticated, we return invite_accepted(). If the user is not authenticated, we raise django.core.exceptions.PermissionDenied.

get_appname()

Get the name of the appname. Must match the appname returned by the get_appname method of your InviteUrl subclass.

invite_accepted(token)

Called on POST when the invite has been accepted by the user.

At this point, self.request.user is the user accepting the invite.

Settings

Required settings:
DJANGO_CRADMIN_SITENAME
The name of the site. You must set this setting unless you override the email subject and message templates as explained in Email templates and how to override them.
Optional settings:
DJANGO_CRADMIN_INVITE_FROM_EMAIL
Defaults to the DEFAULT_FROM_EMAIL setting.