from __future__ import unicode_literals

import logging
import os.path

import django
from django.conf import settings
from django.conf.urls import url
from django.contrib import messages
from django.contrib.auth import forms, get_user_model, views
from django.contrib.auth.models import Permission
from django.contrib.contenttypes.models import ContentType
from django.contrib.postgres.search import SearchVector
from django.core.paginator import EmptyPage, Paginator
from django.db import transaction
from django.forms.models import inlineformset_factory, modelformset_factory
from django.http import HttpResponse
from django.shortcuts import get_object_or_404, redirect
from django.template import RequestContext
from django.template.loader import render_to_string
from django.template.response import TemplateResponse
from django.urls import NoReverseMatch, reverse, reverse_lazy
from django.utils import timezone
from django.utils.encoding import smart_str
from django.utils.http import urlencode
from django.utils.module_loading import import_string
from django.utils.six.moves.urllib.parse import urljoin
from django.utils.translation import ugettext_lazy as _
from django.views.decorators.cache import patch_cache_control
from modelforms.forms import ModelForm
from six.moves import xrange
from touchtechnology.common.decorators import (
    login_required_m, node2extracontext, require_POST_m,
)
from touchtechnology.common.default_settings import PAGINATE_BY
from touchtechnology.common.utils import (
    get_403_or_None, get_all_perms_for_model_cached, get_objects_for_user,
    get_perms_for_model, model_and_manager, select_template_name,
)

logger = logging.getLogger(__name__)


class AlreadyRegistered(Exception):
    pass


class Application(object):

    kwargs_form_class = None
    model_form_bases = (ModelForm,)

    def __init__(self, name, app_name, node=None, **kwargs):
        self.name = name
        self.app_name = app_name
        self.node = node
        # store any instantiation keyword arguments to change behaviour
        self.kwargs = kwargs
        self._spawned = timezone.now()

    def __repr__(self):
        return '<{0}: {1}/{2}/{3}?{4}>'.format(self.__class__.__name__,
                                               self.name, self.app_name,
                                               self.node or '',
                                               urlencode(self.kwargs))

    def _get_namespace(self, cls=None):
        if cls is None:
            cls = self.__class__
        ns, __ = cls.__module__.split('.', 1)
        return ns if ns not in ('apps', self.app_name) else ''

    namespace = property(_get_namespace)

    @property
    def template_base(self):
        # configure the applications base template directory based on
        # namespace and app name
        return os.path.join(self.namespace, self.app_name)

    @property
    def urls(self):
        return self.get_urls(), self.app_name, self.name

    @classmethod
    def verbose_name(cls):
        return cls().app_name.title()

    @property
    def version(self):
        """
        Identify version number of the Application. If it is defined as a
        module we look up the import path until we find the module level
        ``get_version`` function, to a maximum depth of 4 levels.
        """
        ver = ''
        for depth in xrange(1, 5):
            parts = self.__module__.rsplit('.', depth)
            mod = parts[0]
            try:
                ver = import_string("%s.__version__" % mod)
            except ImportError:
                logger.warn('"__version__" not found at "%s"', mod)
            else:
                logger.debug('Identified version "%s" for "%s"', ver, mod)
                break
        name = self.verbose_name
        if callable(name):
            name = name()
        return '{name} {ver}'.format(name=_(name), ver=ver)

    @property
    def current_app(self):
        return self.name

    def reverse(self, name, args=None, kwargs=None, prefix=None):
        if args is None:
            args = ()
        if kwargs is None:
            kwargs = {}
        named_url = '%s:%s' % (self.app_name, name)
        if prefix:
            named_url = '%s:%s' % (prefix, named_url)
        for key in self.kwargs:
            kwargs.pop(key, None)
        return reverse_lazy(named_url, args=args, kwargs=kwargs,
                            current_app=self.current_app)

    def redirect(self, *args, **kwargs):
        "Thin wrapper around django.shortcuts.redirect to save on imports"
        return redirect(*args, **kwargs)

    def _template_path(self, filename, *args):
        args = list(reversed([a for a in args if a]))
        for index, arg in enumerate(args, 1):
            yield os.path.join(
                self.template_base, os.path.join(*args[:index]), filename)
        yield os.path.join(self.template_base, filename)

    def template_path(self, *args, **kwargs):
        return list(self._template_path(*args, **kwargs))

    def generic_permissions(self, request, model,
                            pk=None, instance=None,
                            post_save_redirect=None,
                            templates=None,
                            staff_only=None,
                            max_checkboxes=None,
                            **extra_context):
        # avoid circular import
        from touchtechnology.common.forms.auth import permissionformset_factory

        if instance is None:
            assert pk is not None
            instance = get_object_or_404(model, pk=pk)

        perms = get_perms_for_model(model, add=True)
        has_permission = get_403_or_None(request, perms, instance,
                                         return_403=True,
                                         accept_global_perms=True,
                                         any_perm=False)

        if has_permission and not request.user.is_superuser:
            return has_permission

        extra_context.update({
            'model': model,
        })

        content_type = ContentType.objects.get_for_model(model)
        queryset = Permission.objects.filter(content_type=content_type) \
                                     .exclude(codename__startswith='add_')
        formset_class = permissionformset_factory(
            model, staff_only=staff_only, max_checkboxes=max_checkboxes)

        if templates is None:
            templates = self.template_path(
                'permissions.html', model._meta.model_name)

        return self.generic_edit_multiple(
            request, model,
            queryset=queryset,
            formset_class=formset_class,
            formset_kwargs={
                'instance': instance,
                'queryset': queryset,
            },
            post_save_redirect=post_save_redirect,
            templates=templates,
            extra_context=extra_context)

    def generic_list(self, request, model_or_manager,
                     extra_context=None,
                     object_list_name=None,
                     paginate_by=None,
                     queryset=None,
                     templates=None,
                     list_templates=None,
                     perms=None,
                     permission_required=False,
                     accept_global_perms=True,
                     return_403=None,
                     search=None,
                     *args, **kwargs):

        model, manager = model_and_manager(model_or_manager)

        # If not explicitly declared, redirect the user to login if they are
        # currently anonymous. If they are already identified, we can safely
        # return a HTTP 403 Forbidden.
        if return_403 is None:
            return_403 = not request.user.is_anonymous

        if queryset is None:
            queryset = manager.all()

        # Search should be an iterable of fields to look into.
        if search is not None:
            terms = request.GET.get('search')
            if terms is not None:
                queryset = queryset.annotate(
                    search=SearchVector(*search),
                ).filter(search=terms)

        # Implement permission checking for list of objects, will filter the
        # list to those objects the user is entitled to work on if we do not
        # throw a HTTP 403 for no permission.
        if permission_required:
            if perms is None:
                perms = get_all_perms_for_model_cached(model)

            # Determine the user's permission to see the list using the
            # get_403_or_None - saves decorating view method with
            # @permission_required_or_403
            has_permission = get_403_or_None(
                request, perms, return_403=return_403,
                accept_global_perms=accept_global_perms, any_perm=True)

            global_perms = any([request.user.has_perm(p) for p in perms])

            # If the user does not have any global permissions then adjust the
            # queryset to return only the objects they can act on.
            if not global_perms:
                queryset = get_objects_for_user(
                    request.user, perms, queryset,
                    use_groups=True, any_perm=True)

            # If permission is denied, return the response. May already have
            # thrown an exception by now.
            if not queryset and has_permission is not None:
                return has_permission

        if extra_context is None:
            extra_context = {}

        if object_list_name is None:
            object_list_name = '{0}_list'.format(model._meta.model_name)

        if templates is None:
            templates = self.template_path('list.html',
                                           model._meta.model_name)

        if list_templates is None:
            list_templates = self.template_path('list.inc.html',
                                                model._meta.model_name)
            # Always tack the mvp theme version on the end as a fallback.
            list_templates.append('mvp/list.inc.html')

        # Determine the namespace of this Application so we can construct view
        # names to pass to the template engine to reverse CRUD URI's.
        create_url_name = '%s:add' % request.resolver_match.namespace
        create_url_kwargs = request.resolver_match.kwargs.copy()
        create_url_kwargs.pop('admin', None)
        create_url_kwargs.pop('component', None)

        # We can't leave this until the template is rendered because it throws
        # exceptions (stupid!) so we'll avoid lazy evaluation and pass None if
        # the create view doesn't exist for this model.
        try:
            create_url = reverse(create_url_name,
                                 args=request.resolver_match.args,
                                 kwargs=create_url_kwargs)
        except NoReverseMatch:
            create_url = None

        context = {
            'queryset': queryset,
            object_list_name: queryset,
            'object_list': queryset,
            'is_paginated': False,
            'page': None,
            'page_num': None,
            'paginator': None,
            'model': manager.none(),
            'template': select_template_name(templates),
            'list_template': select_template_name(list_templates),

            # Is search enabled?
            'searchable': bool(search),
            'search': request.GET.get('search', ''),

            # Pass to template the name permissions so we can re-use template
            # code to generically list and add/change/delete objects
            'add_perm': '%s.add_%s' % (model._meta.app_label, model._meta.model_name),
            'view_perm': '%s.view_%s' % (model._meta.app_label, model._meta.model_name),
            'change_perm': '%s.change_%s' % (model._meta.app_label, model._meta.model_name),
            'delete_perm': '%s.delete_%s' % (model._meta.app_label, model._meta.model_name),

            'create_url': create_url,
        }

        if paginate_by is None:
            paginate_by = PAGINATE_BY

        # Ensure that paginate_by is an integer to prevent ZeroDivisionError
        # inside the Django paginator steps.
        paginate_by = int(paginate_by)

        if paginate_by > 0:
            try:
                page_num = int(request.GET.get('page', 1))
            except TypeError:
                page_num = 1

            paginator = Paginator(queryset, paginate_by)

            try:
                page = paginator.page(page_num)
            except EmptyPage:
                page = paginator.page(paginator.num_pages)

            context.update({
                object_list_name: page.object_list,
                'object_list': page.object_list,
                'is_paginated': paginator.num_pages > 1,
                'page': page,
                'page_num': page_num,
                'paginator': paginator,
            })

        context.update(extra_context)
        return self.render(request, templates, context, *args, **kwargs)

    def generic_detail(self, request, model_or_manager,
                       templates=None,
                       extra_context=None,
                       **kwargs):
        model, manager = model_and_manager(model_or_manager)
        instance = get_object_or_404(manager, **kwargs)

        context = {
            'object': instance,
            model._meta.model_name: instance,
            'model': manager.none(),
        }
        context.update(extra_context or {})

        if templates is None:
            templates = self.template_path('detail.html',
                                           model._meta.model_name)

        return self.render(request, templates, context)

    def generic_edit(self, request, model_or_manager,
                     pk=None,
                     instance=None,
                     form_class=None,
                     form_kwargs=None,
                     form_fields='__all__',
                     form_widgets=None,
                     post_save_callback=lambda o: None,
                     post_save_redirect=None,
                     templates=None,
                     changed_messages=(
                         (messages.SUCCESS, _("Your {model} has been "
                                              "saved.")),
                     ),
                     unchanged_messages=(
                         (messages.INFO, _("Your {model} was not changed.")),
                     ),
                     permission_required=False,
                     perms=None,
                     accept_global_perms=True,
                     return_403=None,
                     related=None,
                     extra_context=None):

        model, manager = model_and_manager(model_or_manager)

        # If not explicitly declared, redirect the user to login if they are
        # currently anonymous. If they are already identified, we can safely
        # return a HTTP 403 Forbidden.
        if return_403 is None:
            return_403 = not request.user.is_anonymous

        if instance is None and pk is not None:
            instance = get_object_or_404(manager, pk=pk)

        if form_kwargs is None:
            form_kwargs = {}

        if extra_context is None:
            extra_context = {}

        if post_save_redirect is None:
            redirect_to = urljoin(request.path, '..') \
                if pk is None else request.path
            post_save_redirect = self.redirect(redirect_to)

        # Implement permission checking for specified object instance, throw a
        # HTTP 403 (using django-guardian configuration) when the user is not
        # entitled to edit the instance.
        if permission_required:

            # If no perms are specified, build sensible default using built in
            # permission types that come batteries included with Django.
            if perms is None:
                perms = get_perms_for_model(model, change=True)

                # When there is no pk value, we're doing a creation and should
                # have permission to create the object.
                if pk is None or instance.pk is None:
                    perms = get_perms_for_model(model, add=True)

            # Determine the user's permission to edit this object using the
            # get_403_or_None - saves decorating view method with
            # @permission_required_or_403
            has_permission = get_403_or_None(
                request, perms, obj=instance, return_403=return_403,
                accept_global_perms=accept_global_perms)

            # If permission is denied, return the response. May already have
            # thrown an exception by now.
            if has_permission is not None:
                return has_permission

        # If the developer has not provided a custom form, then dynamically
        # construct a default ModelForm for them.
        if form_class is None:
            meta_class = type(smart_str('Meta'), (), {'model': model,
                                        'fields': form_fields,
                                        'widgets': form_widgets})
            form_class = type(smart_str('EditForm'), self.model_form_bases,
                              {'Meta': meta_class})

        # Whether we've dynamically constructed our form_class or not, check to
        # ensure that we've inherited from all the bases. Log when we haven't,
        # but don't raise any exceptions.
        for base in self.model_form_bases:
            if not issubclass(form_class, base):
                logger.error('"%s" does not inherit "%s"', form_class, base)

        # Pass the instance to the form constructor if this is a ModelForm
        # subclass, otherwise it will need to be explicitly added to
        # form_kwargs if expected.
        if issubclass(form_class, ModelForm) and instance is not None:
            form_kwargs.setdefault('instance', instance)

        # Vanilla form processing here, take the post data and files, create
        # an instance of form_class, save and redirect.
        if request.method == "POST":
            form = form_class(data=request.POST, files=request.FILES,
                              **form_kwargs)
            if form.is_valid():
                replace = dict(
                    model=smart_str(model._meta.verbose_name),
                    models=smart_str(model._meta.verbose_name_plural),
                )
                if form.has_changed():
                    obj = form.save()
                    callback_res = post_save_callback(obj)
                    for level, message in changed_messages:
                        messages.add_message(request, level,
                                             message.format(**replace))
                    if callback_res is not None:
                        return callback_res
                else:
                    for level, message in unchanged_messages:
                        messages.add_message(request, level,
                                             message.format(**replace))
                return post_save_redirect
        else:
            form = form_class(**form_kwargs)

        if templates is None:
            templates = self.template_path('edit.html',
                                           model._meta.model_name)
            templates.append('edit.html')

        context = {
            'model': manager.none(),
            'template': select_template_name(templates),
            'form': form,
            'object': instance,
            'related': related,
            'cancel_url': post_save_redirect.url,
        }
        context.update(extra_context or {})

        return self.render(request, templates, context)

    def generic_edit_multiple(self, request, model_or_manager,
                              extra_context=None,
                              formset_class=None,
                              formset_kwargs=None,
                              formset_fields='__all__',
                              post_save_callback=lambda o: None,
                              post_save_redirect_args=None,
                              post_save_redirect_kwargs=None,
                              post_save_redirect=None,
                              templates=None,
                              changed_messages=None,
                              perms=None,
                              permission_required=False,
                              accept_global_perms=True,
                              return_403=None,
                              *args, **kwargs):

        if extra_context is None:
            extra_context = {}

        if formset_kwargs is None:
            formset_kwargs = {}

        if post_save_redirect_args is None:
            post_save_redirect_args = ()

        if post_save_redirect_kwargs is None:
            post_save_redirect_kwargs = {}

        if changed_messages is None:
            changed_messages = (
                (messages.SUCCESS, _("Your {models} have been saved.")),
            )

        # If not explicitly declared, redirect the user to login if they are
        # currently anonymous. If they are already identified, we can safely
        # return a HTTP 403 Forbidden.
        if return_403 is None:
            return_403 = not request.user.is_anonymous

        # Try and introspect the model if possible.
        model, manager = model_and_manager(model_or_manager)
        queryset = manager.all()

        # Implement permission checking for list of objects, will filter the
        # list to those objects the user is entitled to work on if we do not
        # throw a HTTP 403 for no permission.
        if permission_required:
            if perms is None:
                perms = get_all_perms_for_model_cached(model)

            # Determine the user's permission to see the list using the
            # get_403_or_None - saves decorating view method with
            # @permission_required_or_403
            has_permission = get_403_or_None(
                request, perms, return_403=return_403,
                accept_global_perms=accept_global_perms, any_perm=True)

            # If the user does not have any global permissions then adjust the
            # queryset to return only the objects they can act on.
            if not any([request.user.has_perm(p) for p in perms]):
                queryset = get_objects_for_user(
                    request.user, perms, queryset,
                    use_groups=True, any_perm=True)

            # If permission is denied, return the response. May already have
            # thrown an exception by now.
            if not queryset and has_permission is not None:
                return has_permission

            queryset = get_objects_for_user(
                request.user, perms, queryset, any_perm=True)

        # If queryset is not already specified as a keyword argument to the
        # formset attach it now. Value in formset_kwargs will take precedence.
        queryset = formset_kwargs.pop('queryset', queryset)

        # If the developer has not provided a custom formset, then dynamically
        # construct a default ModelFormSet for them.
        if formset_class is None:
            formset_class = modelformset_factory(model, extra=0,
                                                 fields=formset_fields)

        if post_save_redirect is None:
            redirect_to = urljoin(request.path, '..')
            post_save_redirect = self.redirect(redirect_to)

        # Vanilla formset processing here, take the post data and files, create
        # an instance of formset_class, save and redirect.
        if request.method == "POST":
            formset = formset_class(data=request.POST, files=request.FILES,
                                    queryset=queryset, **formset_kwargs)
            if formset.is_valid():
                replace = dict(
                    model=smart_str(model._meta.verbose_name),
                    models=smart_str(model._meta.verbose_name_plural),
                )
                objects = formset.save()
                map(post_save_callback, objects)
                for level, message in changed_messages:
                    messages.add_message(request, level,
                                         message.format(**replace))
                if isinstance(post_save_redirect, HttpResponse):
                    return post_save_redirect
                redirect_to = self.reverse(post_save_redirect,
                                           args=post_save_redirect_args,
                                           kwargs=post_save_redirect_kwargs)
                return redirect(redirect_to)
        else:
            formset = formset_class(queryset=queryset, **formset_kwargs)

        context = {
            'model': manager.none(),
            'formset': formset,
            'object_list': queryset,
        }
        context.update(extra_context)

        if templates is None:
            templates = self.template_path('edit_multiple.html',
                                           model._meta.model_name)
            templates.append('edit_multiple.html')

        return self.render(request, templates, context)

    def generic_edit_related(self, request, model_or_manager, related_model,
                             extra_context={},
                             form_class=None,
                             form_kwargs={},
                             form_fields='__all__',
                             form_widgets=None,
                             formset_class=None,
                             formset_kwargs={},
                             formset_fields='__all__',
                             instance=None,
                             pk=None,
                             post_save_callback=lambda o: None,
                             post_save_redirect_args=(),
                             post_save_redirect_kwargs={},
                             post_save_redirect='index',
                             post_save_related_callback=lambda o: None,
                             templates=None):

        model, manager = model_and_manager(model_or_manager)

        if instance is None and pk is not None:
            instance = get_object_or_404(manager, pk=pk)

        # If the developer has not provided a custom form, then dynamically
        # construct a default ModelForm for them.
        if form_class is None:
            meta_class = type(smart_str('Meta'), (), {'model': model, 'fields': form_fields, 'widgets': form_widgets})
            form_class = type(smart_str('EditForm'), self.model_form_bases, {'Meta': meta_class})

        # If the developer has not provided a custom formset, then dynamically
        # construct a default one with inlineformset_factory.
        if formset_class is None:
            formset_class = inlineformset_factory(
                model, related_model, fields=formset_fields, extra=0)

        # Add form and formset prefixes if not already present in the kwargs
        form_kwargs.setdefault('prefix', 'form')
        formset_kwargs.setdefault('prefix', 'formset')

        # Keep verbose names for model and related_model for use in messages
        model_verbose_name = model._meta.verbose_name.lower()
        related_verbose_name_plural = \
            related_model._meta.verbose_name_plural.lower()
        verbose = {
            'model_verbose_name': model_verbose_name,
            'related_verbose_name_plural': related_verbose_name_plural,
        }

        # Vanilla form processing here, take the post data and files, create
        # an instance of form_class, save and redirect.
        if request.method == "POST":
            sid = transaction.savepoint()

            form = form_class(data=request.POST, files=request.FILES,
                              instance=instance, **form_kwargs)

            if form.is_valid():
                obj = form.save()
                logger.debug('Saved %r to database.', obj)

                # Rebuild formset with updated instance object.
                formset = formset_class(data=request.POST,
                                        files=request.FILES,
                                        instance=obj, **formset_kwargs)

                if formset.is_valid():
                    logger.debug("Related formset is valid, if we don't "
                                 "complete successfully then an exception "
                                 "will bubble up.")
                    related = formset.save()
                    post_save_callback(obj)
                    map(post_save_related_callback, related)
                    transaction.savepoint_commit(sid)
                    messages.add_message(
                        request, messages.SUCCESS,
                        _("Your %(model_verbose_name)s and "
                          "%(related_verbose_name_plural)s "
                          "have been saved.") % verbose)
                    if isinstance(post_save_redirect, HttpResponse):
                        return post_save_redirect
                    redirect_to = self.reverse(
                        post_save_redirect,
                        args=post_save_redirect_args,
                        kwargs=post_save_redirect_kwargs)
                    return redirect(redirect_to)

                # if we've made it out without returning a HttpRedirect then
                # we should not be persisting the result.
                logger.debug("ROLLBACK, we were not redirected.")
                transaction.savepoint_rollback(sid)

            else:
                formset = formset_class(
                    data=request.POST, files=request.FILES,
                    instance=instance, **formset_kwargs)

        else:
            form = form_class(instance=instance, **form_kwargs)
            formset = formset_class(instance=instance, **formset_kwargs)

        context = {
            'model': manager.none(),
            'form': form,
            'formset': formset,
            'object': instance,
            'media': form.media + formset.media,
        }
        context.update(extra_context)

        if templates is None:
            templates = self.template_path('edit.related.html',
                                           model._meta.model_name)

        return self.render(request, templates, context)

    @require_POST_m
    def generic_delete(self, request, model_or_manager, pk,
                       post_delete_redirect=None,
                       perms=None,
                       permission_required=False,
                       accept_global_perms=True,
                       return_403=None,
                       **kwargs):

        model, manager = model_and_manager(model_or_manager)

        instance = get_object_or_404(manager, pk=pk)

        # If not explicitly declared, redirect the user to login if they are
        # currently anonymous. If they are already identified, we can safely
        # return a HTTP 403 Forbidden.
        if return_403 is None:
            return_403 = not request.user.is_anonymous

        # Implement permission checking for specified object instance, throw a
        # HTTP 403 (using django-guardian configuration) when the user is not
        # entitled to edit the instance.
        if permission_required:

            # If no perms are specified, build sensible default using built in
            # permission types that come batteries included with Django.
            if perms is None:
                perms = get_perms_for_model(model, delete=True)

            # Determine the user's permission to edit this object using the
            # get_403_or_None - saves decorating view method with
            # @permission_required_or_403
            has_permission = get_403_or_None(
                request, perms, obj=instance, return_403=return_403,
                accept_global_perms=accept_global_perms)

            # If permission is denied, return the response. May already have
            # thrown an exception by now.
            if has_permission is not None:
                return has_permission

        if post_delete_redirect is None:
            redirect_to = urljoin(request.path, '../..')
            post_delete_redirect = self.redirect(redirect_to)

        instance.delete()
        messages.add_message(
            request, messages.SUCCESS,
            _("The %s has been deleted.") % model._meta.verbose_name)
        return post_delete_redirect

    def context_instance(self, request, **kwargs):
        return RequestContext(request, current_app=self.current_app, **kwargs)

    def render_to_string(self, request, templates, context, **kwargs):
        context_instance = self.context_instance(request)
        return render_to_string(templates, context, context_instance, **kwargs)

    def render(self, request, templates, context, tz=None, **kwargs):
        logger.debug('{app}.render: {request.path}'.format(
            app=self.__class__.__name__, request=request))
        context.update({'application': self})
        kwargs.setdefault('context', context)

        response = TemplateResponse(request, templates, **kwargs)
        response.current_app = self.current_app

        if tz is not None:
            with timezone.override(tz):
                response.render()

        if not request.user.is_anonymous:
            patch_cache_control(response, private=True)
        return response


class AccountsSite(Application):

    def __init__(self, name='accounts', app_name='accounts', *args, **kwargs):
        super(AccountsSite, self).__init__(name=name, app_name=app_name)
        self.profile_form_class = kwargs.pop(
            'profile_form_class',
            'touchtechnology.common.forms_lazy.ProfileForm')
        self.user_model = get_user_model()

    @property
    def set_password_form(self):
        class SetPasswordForm(forms.SetPasswordForm):
            def __init__(slf, user, *a, **kw):
                super(SetPasswordForm, slf).__init__(user, *a, **kw)
                if user:
                    slf.user = self.user_model.objects.get(pk=user.pk)
        return SetPasswordForm

    @property
    def password_change_form(self):
        class PasswordChangeForm(forms.PasswordChangeForm):
            def __init__(slf, user, *a, **kw):
                super(PasswordChangeForm, slf).__init__(user, *a, **kw)
                if user:
                    slf.user = self.user_model.objects.get(pk=user.pk)
        return PasswordChangeForm

    @property
    def template_base(self):
        return 'registration'

    def get_urls(self):
        urlpatterns = [
            url(r'^login/$', self.login, name='login'),
            url(r'^logout/$', self.logout, name='logout'),
            url(r'^password-change/$',
                self.password_change, name='password_change'),
            url(r'^password-change/done/$',
                self.password_change_done, name='password_change_done'),
            url(r'^password-reset/$',
                self.password_reset, name='password_reset'),
            url(r'^password-reset/done/$',
                self.password_reset_done, name='password_reset_done'),
            url(r'^reset/(?P<uidb64>[0-9A-Za-z_\-]+)/'
                r'(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$',
                self.password_reset_confirm, name='password_reset_confirm'),
            url(r'^reset/done/$',
                self.password_reset_complete, name='password_reset_complete'),
            url(r'^profile/$', self.profile, name='profile'),
            url(r'^$', self.index, name='index'),
        ]
        return urlpatterns

    @node2extracontext
    def login(self, request, *args, **kwargs):
        form_class = getattr(
            settings, 'AUTHENTICATION_FORM_CLASS',
            'django.contrib.auth.forms.AuthenticationForm')
        authentication_form = import_string(form_class)
        if django.VERSION[0] < 2:
            kwargs.setdefault('current_app', self.name)
        return views.login(
            request, authentication_form=authentication_form, *args, **kwargs)

    @node2extracontext
    def logout(self, request, *args, **kwargs):
        if django.VERSION[0] < 2:
            kwargs.setdefault('current_app', self.name)
        return views.logout(request, *args, **kwargs)

    @node2extracontext
    def password_change(self, request, *args, **kwargs):
        post_change_redirect = kwargs.pop(
            'post_change_redirect', self.reverse('password_change_done'))
        password_change_form = kwargs.pop(
            'password_change_form', self.password_change_form)
        return views.password_change(
            request, post_change_redirect=post_change_redirect,
            password_change_form=password_change_form, *args, **kwargs)

    @node2extracontext
    def password_change_done(self, request, *args, **kwargs):
        return views.password_change_done(request, *args, **kwargs)

    @node2extracontext
    def password_reset(self, request, *args, **kwargs):
        post_reset_redirect = kwargs.pop(
            'post_reset_redirect', self.reverse('password_reset_done'))
        return views.password_reset(
            request, post_reset_redirect=post_reset_redirect, *args, **kwargs)

    @node2extracontext
    def password_reset_done(self, request, *args, **kwargs):
        return views.password_reset_done(request, *args, **kwargs)

    @node2extracontext
    def password_reset_confirm(self, request, *args, **kwargs):
        post_reset_redirect = kwargs.pop(
            'post_reset_redirect', self.reverse('password_reset_complete'))
        set_password_form = kwargs.pop(
            'set_password_form', self.set_password_form)
        return views.password_reset_confirm(
            request, post_reset_redirect=post_reset_redirect,
            set_password_form=set_password_form, *args, **kwargs)

    @node2extracontext
    def password_reset_complete(self, request, *args, **kwargs):
        return views.password_reset_complete(request, *args, **kwargs)

    def activation_complete(self, request, *args, **kwargs):
        context = {}
        context.update(kwargs)
        templates = self.template_path('activation_complete.html')
        return self.render(request, templates, context)

    def index(self, request, *args, **kwargs):
        return redirect(settings.LOGIN_REDIRECT_URL)

    @login_required_m
    def profile(self, request, *args, **kwargs):
        form_class = import_string(self.profile_form_class)

        if request.method == 'POST':
            form = form_class(data=request.POST, instance=request.user)
            if form.is_valid():
                if form.has_changed():
                    form.save()
                    messages.add_message(
                        request, messages.SUCCESS,
                        _("Your changes have been saved."))
                else:
                    messages.add_message(
                        request, messages.INFO,
                        _("You did not make any changes."))
                return redirect('.')
        else:
            form = form_class(instance=request.user)

        context = {
            'form': form,
        }
        context.update(kwargs)

        templates = self.template_path('profile.html')
        return self.render(request, templates, context)
