Merge branch 'release/0.9.1' into develop
[philo.git] / philo / contrib / waldo / models.py
index f63cdb1..cdadead 100644 (file)
@@ -1,3 +1,15 @@
+"""
+Waldo provides abstract :class:`.MultiView`\ s to handle several levels of common authentication:
+
+* :class:`LoginMultiView` handles the case where users only need to be able to log in and out.
+* :class:`PasswordMultiView` handles the case where users will also need to change their password.
+* :class:`RegistrationMultiView` builds on top of :class:`PasswordMultiView` to handle user registration, as well.
+* :class:`AccountMultiView` adds account-handling functionality to the :class:`RegistrationMultiView`.
+
+"""
+
+import urlparse
+
 from django import forms
 from django.conf.urls.defaults import url, patterns, include
 from django.contrib import messages
 from django import forms
 from django.conf.urls.defaults import url, patterns, include
 from django.contrib import messages
@@ -15,17 +27,17 @@ from django.utils.http import int_to_base36, base36_to_int
 from django.utils.translation import ugettext as _
 from django.views.decorators.cache import never_cache
 from django.views.decorators.csrf import csrf_protect
 from django.utils.translation import ugettext as _
 from django.views.decorators.cache import never_cache
 from django.views.decorators.csrf import csrf_protect
+
 from philo.models import MultiView, Page
 from philo.contrib.waldo.forms import WaldoAuthenticationForm, RegistrationForm, UserAccountForm
 from philo.contrib.waldo.tokens import registration_token_generator, email_token_generator
 from philo.models import MultiView, Page
 from philo.contrib.waldo.forms import WaldoAuthenticationForm, RegistrationForm, UserAccountForm
 from philo.contrib.waldo.tokens import registration_token_generator, email_token_generator
-import urlparse
 
 
 class LoginMultiView(MultiView):
 
 
 class LoginMultiView(MultiView):
-       """
-       Handles exclusively methods and views related to logging users in and out.
-       """
+       """Handles exclusively methods and views related to logging users in and out."""
+       #: A :class:`ForeignKey` to the :class:`.Page` which will be used to render the login form.
        login_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_login_related')
        login_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_login_related')
+       #: A django form class which will be used for the authentication process. Default: :class:`.WaldoAuthenticationForm`.
        login_form = WaldoAuthenticationForm
        
        @property
        login_form = WaldoAuthenticationForm
        
        @property
@@ -36,7 +48,7 @@ class LoginMultiView(MultiView):
                )
        
        def set_requirement_redirect(self, request, redirect=None):
                )
        
        def set_requirement_redirect(self, request, redirect=None):
-               "Figure out where someone should end up after landing on a `requirement` page like the login page."
+               """Figures out and stores where a user should end up after landing on a page (like the login page) because they have not fulfilled some kind of requirement."""
                if redirect is not None:
                        pass
                elif 'requirement_redirect' in request.session:
                if redirect is not None:
                        pass
                elif 'requirement_redirect' in request.session:
@@ -61,6 +73,7 @@ class LoginMultiView(MultiView):
                request.session['requirement_redirect'] = redirect
        
        def get_requirement_redirect(self, request, default=None):
                request.session['requirement_redirect'] = redirect
        
        def get_requirement_redirect(self, request, default=None):
+               """Returns the location which a user should be redirected to after fulfilling a requirement (like logging in)."""
                redirect = request.session.pop('requirement_redirect', None)
                # Security checks a la django.contrib.auth.views.login
                if not redirect or ' ' in redirect:
                redirect = request.session.pop('requirement_redirect', None)
                # Security checks a la django.contrib.auth.views.login
                if not redirect or ' ' in redirect:
@@ -75,9 +88,7 @@ class LoginMultiView(MultiView):
        
        @never_cache
        def login(self, request, extra_context=None):
        
        @never_cache
        def login(self, request, extra_context=None):
-               """
-               Displays the login form for the given HttpRequest.
-               """
+               """Renders the :attr:`login_page` with an instance of the :attr:`login_form` for the given :class:`HttpRequest`."""
                self.set_requirement_redirect(request)
                
                # Redirect already-authenticated users to the index page.
                self.set_requirement_redirect(request)
                
                # Redirect already-authenticated users to the index page.
@@ -96,7 +107,7 @@ class LoginMultiView(MultiView):
                                
                                return HttpResponseRedirect(redirect)
                else:
                                
                                return HttpResponseRedirect(redirect)
                else:
-                       form = self.login_form()
+                       form = self.login_form(request)
                
                request.session.set_test_cookie()
                
                
                request.session.set_test_cookie()
                
@@ -109,9 +120,11 @@ class LoginMultiView(MultiView):
        
        @never_cache
        def logout(self, request, extra_context=None):
        
        @never_cache
        def logout(self, request, extra_context=None):
+               """Logs the given :class:`HttpRequest` out, redirecting the user to the page they just left or to the :meth:`~.Node.get_absolute_url` for the ``request.node``."""
                return auth_views.logout(request, request.META.get('HTTP_REFERER', request.node.get_absolute_url()))
        
        def login_required(self, view):
                return auth_views.logout(request, request.META.get('HTTP_REFERER', request.node.get_absolute_url()))
        
        def login_required(self, view):
+               """Wraps a view function to require that the user be logged in."""
                def inner(request, *args, **kwargs):
                        if not request.user.is_authenticated():
                                self.set_requirement_redirect(request, redirect=request.path)
                def inner(request, *args, **kwargs):
                        if not request.user.is_authenticated():
                                self.set_requirement_redirect(request, redirect=request.path)
@@ -127,33 +140,55 @@ class LoginMultiView(MultiView):
 
 
 class PasswordMultiView(LoginMultiView):
 
 
 class PasswordMultiView(LoginMultiView):
-       "Adds on views for password-related functions."
+       """
+       Adds support for password setting, resetting, and changing to the :class:`LoginMultiView`. Password reset support includes handling of a confirmation email.
+       
+       """
+       #: A :class:`ForeignKey` to the :class:`.Page` which will be used to render the password reset request form.
        password_reset_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_password_reset_related', blank=True, null=True)
        password_reset_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_password_reset_related', blank=True, null=True)
+       #: A :class:`ForeignKey` to the :class:`.Page` which will be used to render the password reset confirmation email.
        password_reset_confirmation_email = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_password_reset_confirmation_email_related', blank=True, null=True)
        password_reset_confirmation_email = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_password_reset_confirmation_email_related', blank=True, null=True)
+       #: A :class:`ForeignKey` to the :class:`.Page` which will be used to render the password setting form (i.e. the page that users will see after confirming a password reset).
        password_set_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_password_set_related', blank=True, null=True)
        password_set_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_password_set_related', blank=True, null=True)
+       #: A :class:`ForeignKey` to the :class:`.Page` which will be used to render the password change form.
        password_change_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_password_change_related', blank=True, null=True)
        
        password_change_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_password_change_related', blank=True, null=True)
        
+       #: The password change form class. Default: :class:`django.contrib.auth.forms.PasswordChangeForm`.
        password_change_form = PasswordChangeForm
        password_change_form = PasswordChangeForm
+       #: The password set form class. Default: :class:`django.contrib.auth.forms.SetPasswordForm`.
        password_set_form = SetPasswordForm
        password_set_form = SetPasswordForm
+       #: The password reset request form class. Default: :class:`django.contrib.auth.forms.PasswordResetForm`.
        password_reset_form = PasswordResetForm
        
        @property
        def urlpatterns(self):
                urlpatterns = super(PasswordMultiView, self).urlpatterns
                
        password_reset_form = PasswordResetForm
        
        @property
        def urlpatterns(self):
                urlpatterns = super(PasswordMultiView, self).urlpatterns
                
-               if self.password_reset_page and self.password_reset_confirmation_email and self.password_set_page:
+               if self.password_reset_page_id and self.password_reset_confirmation_email_id and self.password_set_page_id:
                        urlpatterns += patterns('',
                                url(r'^password/reset$', csrf_protect(self.password_reset), name='password_reset'),
                                url(r'^password/reset/(?P<uidb36>\w+)/(?P<token>[^/]+)$', self.password_reset_confirm, name='password_reset_confirm'),
                        )
                
                        urlpatterns += patterns('',
                                url(r'^password/reset$', csrf_protect(self.password_reset), name='password_reset'),
                                url(r'^password/reset/(?P<uidb36>\w+)/(?P<token>[^/]+)$', self.password_reset_confirm, name='password_reset_confirm'),
                        )
                
-               if self.password_change_page:
+               if self.password_change_page_id:
                        urlpatterns += patterns('',
                                url(r'^password/change$', csrf_protect(self.login_required(self.password_change)), name='password_change'),
                        )
                return urlpatterns
        
        def make_confirmation_link(self, confirmation_view, token_generator, user, node, token_args=None, reverse_kwargs=None, secure=False):
                        urlpatterns += patterns('',
                                url(r'^password/change$', csrf_protect(self.login_required(self.password_change)), name='password_change'),
                        )
                return urlpatterns
        
        def make_confirmation_link(self, confirmation_view, token_generator, user, node, token_args=None, reverse_kwargs=None, secure=False):
+               """
+               Generates a confirmation link for an arbitrary action, such as a password reset.
+               
+               :param confirmation_view: The view function which needs to be linked to.
+               :param token_generator: Generates a confirmable token for the action.
+               :param user: The user who is trying to take the action.
+               :param node: The node which is providing the basis for the confirmation URL.
+               :param token_args: A list of additional arguments (i.e. besides the user) to be used for token creation.
+               :param reverse_kwargs: A dictionary of any additional keyword arguments necessary for correctly reversing the view.
+               :param secure: Whether the link should use the https:// or http://.
+               
+               """
                token = token_generator.make_token(user, *(token_args or []))
                kwargs = {
                        'uidb36': int_to_base36(user.id),
                token = token_generator.make_token(user, *(token_args or []))
                kwargs = {
                        'uidb36': int_to_base36(user.id),
@@ -163,6 +198,15 @@ class PasswordMultiView(LoginMultiView):
                return node.construct_url(subpath=self.reverse(confirmation_view, kwargs=kwargs), with_domain=True, secure=secure)
        
        def send_confirmation_email(self, subject, email, page, extra_context):
                return node.construct_url(subpath=self.reverse(confirmation_view, kwargs=kwargs), with_domain=True, secure=secure)
        
        def send_confirmation_email(self, subject, email, page, extra_context):
+               """
+               Sends a confirmation email for an arbitrary action, such as a password reset. If the ``page``'s :class:`.Template` has a mimetype of ``text/html``, then the email will be sent with an HTML alternative version.
+               
+               :param subject: The subject line of the email.
+               :param email: The recipient's address.
+               :param page: The page which will be used to render the email body.
+               :param extra_context: The context for rendering the ``page``.
+               
+               """
                text_content = page.render_to_string(extra_context=extra_context)
                from_email = 'noreply@%s' % Site.objects.get_current().domain
                
                text_content = page.render_to_string(extra_context=extra_context)
                from_email = 'noreply@%s' % Site.objects.get_current().domain
                
@@ -174,6 +218,21 @@ class PasswordMultiView(LoginMultiView):
                        send_mail(subject, text_content, from_email, [email])
        
        def password_reset(self, request, extra_context=None, token_generator=password_token_generator):
                        send_mail(subject, text_content, from_email, [email])
        
        def password_reset(self, request, extra_context=None, token_generator=password_token_generator):
+               """
+               Handles the process by which users request a password reset, and generates the context for the confirmation email. That context will contain:
+               
+               link
+                       The confirmation link for the password reset.
+               user
+                       The user requesting the reset.
+               site
+                       The current :class:`Site`.
+               request
+                       The current :class:`HttpRequest` instance.
+               
+               :param token_generator: The token generator to use for the confirmation link.
+               
+               """
                if request.user.is_authenticated():
                        return HttpResponseRedirect(request.node.get_absolute_url())
                
                if request.user.is_authenticated():
                        return HttpResponseRedirect(request.node.get_absolute_url())
                
@@ -186,10 +245,7 @@ class PasswordMultiView(LoginMultiView):
                                                'link': self.make_confirmation_link('password_reset_confirm', token_generator, user, request.node, secure=request.is_secure()),
                                                'user': user,
                                                'site': current_site,
                                                'link': self.make_confirmation_link('password_reset_confirm', token_generator, user, request.node, secure=request.is_secure()),
                                                'user': user,
                                                'site': current_site,
-                                               'request': request,
-                                               
-                                               # Deprecated... leave in for backwards-compatibility
-                                               'username': user.username
+                                               'request': request
                                        }
                                        self.send_confirmation_email('Confirm password reset for account at %s' % current_site.domain, user.email, self.password_reset_confirmation_email, context)
                                        messages.add_message(request, messages.SUCCESS, "An email has been sent to the address you provided with details on resetting your password.", fail_silently=True)
                                        }
                                        self.send_confirmation_email('Confirm password reset for account at %s' % current_site.domain, user.email, self.password_reset_confirmation_email, context)
                                        messages.add_message(request, messages.SUCCESS, "An email has been sent to the address you provided with details on resetting your password.", fail_silently=True)
@@ -206,8 +262,10 @@ class PasswordMultiView(LoginMultiView):
        
        def password_reset_confirm(self, request, extra_context=None, uidb36=None, token=None, token_generator=password_token_generator):
                """
        
        def password_reset_confirm(self, request, extra_context=None, uidb36=None, token=None, token_generator=password_token_generator):
                """
-               Checks that a given hash in a password reset link is valid. If so,
-               displays the password set form.
+               Checks that ``token``` is valid, and if so, renders an instance of :attr:`password_set_form` with :attr:`password_set_page`.
+               
+               :param token_generator: The token generator used to check the ``token``.
+               
                """
                assert uidb36 is not None and token is not None
                try:
                """
                assert uidb36 is not None and token is not None
                try:
@@ -238,6 +296,7 @@ class PasswordMultiView(LoginMultiView):
                raise Http404
        
        def password_change(self, request, extra_context=None):
                raise Http404
        
        def password_change(self, request, extra_context=None):
+               """Renders an instance of :attr:`password_change_form` with :attr:`password_change_page`."""
                if request.method == 'POST':
                        form = self.password_change_form(request.user, request.POST)
                        if form.is_valid():
                if request.method == 'POST':
                        form = self.password_change_form(request.user, request.POST)
                        if form.is_valid():
@@ -259,15 +318,18 @@ class PasswordMultiView(LoginMultiView):
 
 
 class RegistrationMultiView(PasswordMultiView):
 
 
 class RegistrationMultiView(PasswordMultiView):
-       """Adds on the pages necessary for letting new users register."""
+       """Adds support for user registration to the :class:`PasswordMultiView`."""
+       #: A :class:`ForeignKey` to the :class:`.Page` which will be used to display the registration form.
        register_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_register_related', blank=True, null=True)
        register_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_register_related', blank=True, null=True)
+       #: A :class:`ForeignKey` to the :class:`.Page` which will be used to render the registration confirmation email.
        register_confirmation_email = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_register_confirmation_email_related', blank=True, null=True)
        register_confirmation_email = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_register_confirmation_email_related', blank=True, null=True)
+       #: The registration form class. Default: :class:`.RegistrationForm`.
        registration_form = RegistrationForm
        
        @property
        def urlpatterns(self):
                urlpatterns = super(RegistrationMultiView, self).urlpatterns
        registration_form = RegistrationForm
        
        @property
        def urlpatterns(self):
                urlpatterns = super(RegistrationMultiView, self).urlpatterns
-               if self.register_page and self.register_confirmation_email:
+               if self.register_page_id and self.register_confirmation_email_id:
                        urlpatterns += patterns('',
                                url(r'^register$', csrf_protect(self.register), name='register'),
                                url(r'^register/(?P<uidb36>\w+)/(?P<token>[^/]+)$', self.register_confirm, name='register_confirm')
                        urlpatterns += patterns('',
                                url(r'^register$', csrf_protect(self.register), name='register'),
                                url(r'^register/(?P<uidb36>\w+)/(?P<token>[^/]+)$', self.register_confirm, name='register_confirm')
@@ -275,6 +337,12 @@ class RegistrationMultiView(PasswordMultiView):
                return urlpatterns
        
        def register(self, request, extra_context=None, token_generator=registration_token_generator):
                return urlpatterns
        
        def register(self, request, extra_context=None, token_generator=registration_token_generator):
+               """
+               Renders the :attr:`register_page` with an instance of :attr:`registration_form` in the context as ``form``. If the form has been submitted, sends a confirmation email using :attr:`register_confirmation_email` and the same context as :meth:`PasswordMultiView.password_reset`.
+               
+               :param token_generator: The token generator to use for the confirmation link.
+               
+               """
                if request.user.is_authenticated():
                        return HttpResponseRedirect(request.node.get_absolute_url())
                
                if request.user.is_authenticated():
                        return HttpResponseRedirect(request.node.get_absolute_url())
                
@@ -304,9 +372,9 @@ class RegistrationMultiView(PasswordMultiView):
        
        def register_confirm(self, request, extra_context=None, uidb36=None, token=None, token_generator=registration_token_generator):
                """
        
        def register_confirm(self, request, extra_context=None, uidb36=None, token=None, token_generator=registration_token_generator):
                """
-               Checks that a given hash in a registration link is valid and activates
-               the given account. If so, log them in and redirect to
-               self.post_register_confirm_redirect.
+               Checks that ``token`` is valid, and if so, logs the user in and redirects them to :meth:`post_register_confirm_redirect`.
+               
+               :param token_generator: The token generator used to check the ``token``.
                """
                assert uidb36 is not None and token is not None
                try:
                """
                assert uidb36 is not None and token is not None
                try:
@@ -333,6 +401,7 @@ class RegistrationMultiView(PasswordMultiView):
                raise Http404
        
        def post_register_confirm_redirect(self, request):
                raise Http404
        
        def post_register_confirm_redirect(self, request):
+               """Returns an :class:`HttpResponseRedirect` for post-registration-confirmation. Default: :meth:`Node.get_absolute_url` for ``request.node``."""
                return HttpResponseRedirect(request.node.get_absolute_url())
        
        class Meta:
                return HttpResponseRedirect(request.node.get_absolute_url())
        
        class Meta:
@@ -340,45 +409,43 @@ class RegistrationMultiView(PasswordMultiView):
 
 
 class AccountMultiView(RegistrationMultiView):
 
 
 class AccountMultiView(RegistrationMultiView):
-       """
-       By default, the `account` consists of the first_name, last_name, and email fields
-       of the User model. Using a different account model is as simple as writing a form that
-       accepts a User instance as the first argument.
-       """
+       """Adds support for user accounts on top of the :class:`RegistrationMultiView`. By default, the account consists of the first_name, last_name, and email fields of the User model. Using a different account model is as simple as replacing :attr:`account_form` with any form class that takes an :class:`auth.User` instance as the first argument."""
+       #: A :class:`ForeignKey` to the :class:`Page` which will be used to render the account management form.
        manage_account_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_manage_account_related', blank=True, null=True)
        manage_account_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_manage_account_related', blank=True, null=True)
+       #: A :class:`ForeignKey` to a :class:`Page` which will be used to render an email change confirmation email. This is optional; if it is left blank, then email changes will be performed without confirmation.
        email_change_confirmation_email = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_email_change_confirmation_email_related', blank=True, null=True, help_text="If this is left blank, email changes will be performed without confirmation.")
        
        email_change_confirmation_email = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_email_change_confirmation_email_related', blank=True, null=True, help_text="If this is left blank, email changes will be performed without confirmation.")
        
+       #: A django form class which will be used to manage the user's account. Default: :class:`.UserAccountForm`
        account_form = UserAccountForm
        
        @property
        def urlpatterns(self):
                urlpatterns = super(AccountMultiView, self).urlpatterns
        account_form = UserAccountForm
        
        @property
        def urlpatterns(self):
                urlpatterns = super(AccountMultiView, self).urlpatterns
-               if self.manage_account_page:
+               if self.manage_account_page_id:
                        urlpatterns += patterns('',
                                url(r'^account$', self.login_required(self.account_view), name='account'),
                        )
                        urlpatterns += patterns('',
                                url(r'^account$', self.login_required(self.account_view), name='account'),
                        )
-               if self.email_change_confirmation_email:
+               if self.email_change_confirmation_email_id:
                        urlpatterns += patterns('',
                                url(r'^account/email/(?P<uidb36>\w+)/(?P<email>[\w.]+[+][\w.]+)/(?P<token>[^/]+)$', self.email_change_confirm, name='email_change_confirm')
                        )
                return urlpatterns
        
        def account_view(self, request, extra_context=None, token_generator=email_token_generator, *args, **kwargs):
                        urlpatterns += patterns('',
                                url(r'^account/email/(?P<uidb36>\w+)/(?P<email>[\w.]+[+][\w.]+)/(?P<token>[^/]+)$', self.email_change_confirm, name='email_change_confirm')
                        )
                return urlpatterns
        
        def account_view(self, request, extra_context=None, token_generator=email_token_generator, *args, **kwargs):
+               """
+               Renders the :attr:`manage_account_page` with an instance of :attr:`account_form` in the context as ``form``. If the form has been posted, the user's email was changed, and :attr:`email_change_confirmation_email` is not ``None``, sends a confirmation email to the new email to make sure it exists before making the change. The email will have the same context as :meth:`PasswordMultiView.password_reset`.
+               
+               :param token_generator: The token generator to use for the confirmation link. 
+               
+               """
                if request.method == 'POST':
                        form = self.account_form(request.user, request.POST, request.FILES)
                        
                        if form.is_valid():
                                message = "Account information saved."
                                redirect = self.get_requirement_redirect(request, default='')
                if request.method == 'POST':
                        form = self.account_form(request.user, request.POST, request.FILES)
                        
                        if form.is_valid():
                                message = "Account information saved."
                                redirect = self.get_requirement_redirect(request, default='')
-                               if 'email' in form.changed_data and self.email_change_confirmation_email:
-                                       # ModelForms modify their instances in-place during
-                                       # validation, so reset the instance's email to its
-                                       # previous value here, then remove the new value
-                                       # from cleaned_data. We only do this if an email
-                                       # change confirmation email is available.
-                                       request.user.email = form.initial['email']
-                                       
-                                       email = form.cleaned_data.pop('email')
+                               if form.email_changed() and self.email_change_confirmation_email:
+                                       email = form.reset_email()
                                        
                                        current_site = Site.objects.get_current()
                                        
                                        
                                        current_site = Site.objects.get_current()
                                        
@@ -390,7 +457,7 @@ class AccountMultiView(RegistrationMultiView):
                                        }
                                        self.send_confirmation_email('Confirm account email change at %s' % current_site.domain, email, self.email_change_confirmation_email, context)
                                        
                                        }
                                        self.send_confirmation_email('Confirm account email change at %s' % current_site.domain, email, self.email_change_confirmation_email, context)
                                        
-                                       message = "An email has be sent to %s to confirm the email%s." % (email, bool(request.user.email) and " change" or "")
+                                       message = "An email has be sent to %s to confirm the email%s." % (email, " change" if bool(request.user.email) else "")
                                        if not request.user.email:
                                                message += " You will need to confirm the email before accessing pages that require a valid account."
                                                redirect = ''
                                        if not request.user.email:
                                                message += " You will need to confirm the email before accessing pages that require a valid account."
                                                redirect = ''
@@ -413,11 +480,13 @@ class AccountMultiView(RegistrationMultiView):
                return self.manage_account_page.render_to_response(request, extra_context=context)
        
        def has_valid_account(self, user):
                return self.manage_account_page.render_to_response(request, extra_context=context)
        
        def has_valid_account(self, user):
+               """Returns ``True`` if the ``user`` has a valid account and ``False`` otherwise."""
                form = self.account_form(user, {})
                form.data = form.initial
                return form.is_valid()
        
        def account_required(self, view):
                form = self.account_form(user, {})
                form.data = form.initial
                return form.is_valid()
        
        def account_required(self, view):
+               """Wraps a view function to allow access only to users with valid accounts and otherwise redirect them to the :meth:`account_view`."""
                def inner(request, *args, **kwargs):
                        if not self.has_valid_account(request.user):
                                messages.add_message(request, messages.ERROR, "You need to add some account information before you can access that page.", fail_silently=True)
                def inner(request, *args, **kwargs):
                        if not self.has_valid_account(request.user):
                                messages.add_message(request, messages.ERROR, "You need to add some account information before you can access that page.", fail_silently=True)
@@ -425,7 +494,7 @@ class AccountMultiView(RegistrationMultiView):
                                        self.set_requirement_redirect(request, redirect=request.path)
                                        redirect = self.reverse('account', node=request.node)
                                else:
                                        self.set_requirement_redirect(request, redirect=request.path)
                                        redirect = self.reverse('account', node=request.node)
                                else:
-                                       redirect = node.get_absolute_url()
+                                       redirect = request.node.get_absolute_url()
                                return HttpResponseRedirect(redirect)
                        return view(request, *args, **kwargs)
                
                                return HttpResponseRedirect(redirect)
                        return view(request, *args, **kwargs)
                
@@ -433,6 +502,7 @@ class AccountMultiView(RegistrationMultiView):
                return inner
        
        def post_register_confirm_redirect(self, request):
                return inner
        
        def post_register_confirm_redirect(self, request):
+               """Automatically redirects users to the :meth:`account_view` after registration."""
                if self.manage_account_page:
                        messages.add_message(request, messages.INFO, 'Welcome! Please fill in some more information.', fail_silently=True)
                        return HttpResponseRedirect(self.reverse('account', node=request.node))
                if self.manage_account_page:
                        messages.add_message(request, messages.INFO, 'Welcome! Please fill in some more information.', fail_silently=True)
                        return HttpResponseRedirect(self.reverse('account', node=request.node))
@@ -440,7 +510,10 @@ class AccountMultiView(RegistrationMultiView):
        
        def email_change_confirm(self, request, extra_context=None, uidb36=None, token=None, email=None, token_generator=email_token_generator):
                """
        
        def email_change_confirm(self, request, extra_context=None, uidb36=None, token=None, email=None, token_generator=email_token_generator):
                """
-               Checks that a given hash in an email change link is valid. If so, changes the email and redirects to the account page.
+               Checks that ``token`` is valid, and if so, changes the user's email.
+               
+               :param token_generator: The token generator used to check the ``token``.
+               
                """
                assert uidb36 is not None and token is not None and email is not None
                
                """
                assert uidb36 is not None and token is not None and email is not None
                
@@ -458,8 +531,7 @@ class AccountMultiView(RegistrationMultiView):
                        raise Http404
                
                if token_generator.check_token(user, email, token):
                        raise Http404
                
                if token_generator.check_token(user, email, token):
-                       user.email = email
-                       user.save()
+                       self.account_form.set_email(user, email)
                        messages.add_message(request, messages.SUCCESS, 'Email changed successfully.')
                        if self.manage_account_page:
                                redirect = self.reverse('account', node=request.node)
                        messages.add_message(request, messages.SUCCESS, 'Email changed successfully.')
                        if self.manage_account_page:
                                redirect = self.reverse('account', node=request.node)