Using django-generic-confirmation

django-generic-confirmation tries to solve a few real use-cases I stumpled upon in different projects in a generic way.

Use-cases

Use-cases include but are not limited to:

  • New user signs up and the account should only be activated after the email address is confirmed by clicking a link in an email sent to the user.
  • An already registered user wants to change his email address. Bevor doing the change, the user gets an email and has to click a link to confirm the change.
  • A user enters his mobile phone numer. To confirm, that the user owns the number, a short random code is sent via sms and the user has to enter the code into a form to confirm the number.
  • A postal address should be confirmed by sending the user a letter with a short random code, the user receives the letter and has to enter the code into a form to confirm his address.
  • Same as above for bank accounts. You send the user a small amount of money together with a short code.

django-generic-confirmation will solve this use-cases and make it easy for you to integrate this and any other things into your project with minimal effort.

Based on these use-cases a pattern of deferred modelform saving emerged.

Inheriting from DeferredForm you get a ModelForm which will not modify or create the object on save() but will store everything needed to finish the action in the database identified by a random token. Later, whenever this token is received by the application (and if it’s not expired), the form will be saved as if the data was received just now instead of the token.

One caveat: If a form is defered which involves ForeignKey oder ManyToMany Fields and the related objects are deleted while the form is deferred, than an error will be raised on confirmation.

For the developer the DeferredForm works like a normal ModelForm and all you have to do to is write your view as if it should change the data right now, under the hood it is not executed until it is confirmed.

I created this project because I think too many apps exist to solve a very similar problem. Take a look at:

  • django-registration: Creates User objects and activates them if the email is confirmed by visiting a random link sent to the email address.
  • django-email-confirmation: Based on django-registration, but used to manage the case, where a user has more than one email address and every new address has to be confirmed.
  • django-confirmation: based on django-registration and django-email-registration, exists to confirm object-creation via email, much like django-registration but a bit more flexible because any django model can be used. Marks objects as confirmed by changing a field on the object.

API example

This is the code you have to write to turn some normal editing workflow into a edit-and-confirm workflow.

def my_view(request):
    """
    example view to change the user's email address
    """
    if request.method == 'POST':
        form = EmailChangeForm(request.POST, instance=request.user)
        if form.is_valid():
            form.save()
            # redirect user and display a message explaining how to
            # confirm the change
            # request.user.message_set.create(message=u"...")
            # return HttpResponseRedirect(...)
            # _or_
            # send user to a page which explains the confirmation process
            return render_to_response(...)
    else:
        form = EmailChangeForm()
    return render_to_response(...)

the magic happens in the form, which should be inherited from generic_confirmation.forms.DeferredForm.

class EmailChangeForm(DeferredForm):
    class Meta:
        model = User
        fields = ('email',)

your url-conf:

(r'^confirm/', include('generic_confirmation.urls'))

Notification

The one part left is how to get the random token to the user who has to confirm the change. This does not neccessarily has to be the user who made the change in the first step. This could also be some site-moderator for example.

Notification of someone about some action which should be confirmed can happen via different channels, most notably email and sms. django-notification would be great for this, but the pluggable-backend branch is not finished yet (isn’t it?) ...

Signal-based notification

To make a long story short, currently only the signal confirmation_required is fired and it’s your task to listen for it and take the appropriate action.

from generic_confirmation import signals

def send_notification(sender, instance, **kwargs):
        """ a signal receiver which does some tests """
        print sender # the class which is edited
        print instance # the DeferredAction
        print instance.token # needed to confirm the action


signals.confirmation_required.connect(send_notification)

If you pass user=request.user to the form.save()-method, then the signal will provide a user argument pointing to the user who requested the change. It’s also possible to pass another user to save method to inform an admin or so.

def send_notification(sender, instance, user, **kwargs):
    body = render_to_string('confirmation_mail.html', {'user': user, 'token': instance.token})
    send_mail("subject", body, recipient_list=[user.email,])
    # _or_
    # send_mail("subject", body, recipient_list)

signals.confirmation_required.connect(send_notification)

Form-based notification

The second way is to provide a method named send_notification on the form-class itself. The method takes the request.user (if given) and the instance which should be confirmed as arguments.

class EmailChangeForm(DeferredForm):
    def send_notification(self, user=None, instance=None):
        send_mail("please confirm your new address",
            render_to_string("confirm_mail.txt", {'token': instance.token, 'user': user}),
            recipient_list=[self.cleaned_data['email'],])

    class Meta:
        model = User
        fields = ('email',)



class PhoneNumberChangeForm(DeferredForm):
    def send_notification(self, user=None, instance=None):
        send_sms({'to': self.cleaned_data['mobile_number'],
                'text': render_to_string('confirm_phone.txt',
                        {'token': instance.token, 'user': user})
                })
    class Meta:
        model = UserProfile
        fields = ('mobile_number',)

Some of the default notification methods will be provided as mixin classes soon.