

import json
import operator
from functools import reduce

import django.forms.models as model_forms
from django import forms
from django.conf import settings
from django.conf.urls import url
from django.contrib import messages
from django.contrib.auth import REDIRECT_FIELD_NAME
from django.core.exceptions import ImproperlyConfigured
from django.urls import reverse
from django.db import IntegrityError
from django.db.models import Q
from django.http import HttpResponseRedirect, HttpResponse, JsonResponse
from django.utils.encoding import force_text
from django.utils.http import urlquote
from django.utils.translation import ugettext_lazy as _
from django.views.generic.edit import ModelFormMixin, UpdateView, CreateView, ProcessFormView, FormView
from django.views.generic.base import TemplateView
from django.views.generic import DetailView, ListView

from smartmin.csv_imports.models import ImportTask
from smartmin.mixins import NonAtomicMixin

from . import widgets


def smart_url(url, obj=None):
    """
    URLs that start with @ are reversed, using the passed in arguments.

    Otherwise a straight % substitution is applied.
    """
    if url.find("@") >= 0:
        (args, value) = url.split('@')

        if args:
            val = getattr(obj, args, None)
            return reverse(value, args=[val])
        else:
            return reverse(value)
    else:
        if obj is None:
            return url
        else:
            return url % obj.id


class SmartView:
    fields = None
    exclude = None
    field_config = {}
    title = None
    refresh = 0
    template_name = None
    pjax = None

    # set by our CRUDL
    url_name = None

    # if we are part of a CRUDL, we keep a reference to it here, set by CRUDL
    crudl = None

    def __init__(self, *args):
        """
        There are a few variables we want to mantain in the instance, not the
        class.
        """
        self.extra_context = {}
        super(SmartView, self).__init__()

    def derive_title(self):
        """
        Returns the title used on this page.
        """
        return self.title

    @classmethod
    def derive_url_pattern(cls, path, action):
        """
        Returns the URL pattern for this view.
        """
        return r'^%s/%s/$' % (path, action)

    def has_permission(self, request, *args, **kwargs):
        """
        Figures out if the current user has permissions for this view.
        """
        self.kwargs = kwargs
        self.args = args
        self.request = request

        if not getattr(self, 'permission', None):
            return True
        else:
            return request.user.has_perm(self.permission)

    def dispatch(self, request, *args, **kwargs):
        """
        Overloaded to check permissions if appropriate
        """
        def wrapper(request, *args, **kwargs):
            if not self.has_permission(request, *args, **kwargs):
                path = urlquote(request.get_full_path())
                login_url = kwargs.pop('login_url', settings.LOGIN_URL)
                redirect_field_name = kwargs.pop('redirect_field_name', REDIRECT_FIELD_NAME)
                return HttpResponseRedirect("%s?%s=%s" % (login_url, redirect_field_name, path))
            else:
                response = self.pre_process(request, *args, **kwargs)
                if not response:
                    return super(SmartView, self).dispatch(request, *args, **kwargs)
                else:
                    return response

        return wrapper(request, *args, **kwargs)

    def pre_process(self, request, *args, **kwargs):
        """
        Gives the view an opportunity to intercept this response and return a different
        response instead.  This can be used to check some precondition for example and to
        redirect the user somewhere else if they are not met.

        Views which wish to use this should return a Response object.
        """
        return None

    def lookup_obj_attribute(self, obj, field):
        """
        Looks for a field's value from the passed in obj.  Note that this will strip
        leading attributes to deal with subelements if possible
        """
        curr_field = field.encode('ascii', 'ignore').decode("utf-8")
        rest = None

        if field.find('.') >= 0:
            curr_field = field.split('.')[0]
            rest = '.'.join(field.split('.')[1:])

        # next up is the object itself
        obj_field = getattr(obj, curr_field, None)

        # if it is callable, do so
        if obj_field and getattr(obj_field, '__call__', None):
            obj_field = obj_field()

        if obj_field and rest:
            return self.lookup_obj_attribute(obj_field, rest)
        else:
            return obj_field

    def lookup_field_value(self, context, obj, field):
        """
        Looks up the field value for the passed in object and field name.

        Note that this method is actually called from a template, but this provides a hook
        for subclasses to modify behavior if they wish to do so.

        This may be used for example to change the display value of a variable depending on
        other variables within our context.
        """
        curr_field = field.encode('ascii', 'ignore').decode("utf-8")

        # if this isn't a subfield, check the view to see if it has a get_ method
        if field.find('.') == -1:
            # view supercedes all, does it have a 'get_' method for this obj
            view_method = getattr(self, 'get_%s' % curr_field, None)
            if view_method:
                return view_method(obj)

        return self.lookup_obj_attribute(obj, field)

    def lookup_field_label(self, context, field, default=None):
        """
        Figures out what the field label should be for the passed in field name.

        Our heuristic is as follows:
            1) we check to see if our field_config has a label specified
            2) if not, then we derive a field value from the field name
        """
        # if this is a subfield, strip off everything but the last field name
        if field.find('.') >= 0:
            return self.lookup_field_label(context, field.split('.')[-1], default)

        label = None

        # is there a label specified for this field
        if field in self.field_config and 'label' in self.field_config[field]:
            label = self.field_config[field]['label']

        # if we were given a default, use that
        elif default:
            label = default

        # check our model
        else:
            for model_field in self.model._meta.fields:
                if model_field.name == field:
                    return model_field.verbose_name.title()

        # otherwise, derive it from our field name
        if label is None:
            label = self.derive_field_label(field)

        return label

    def lookup_field_help(self, field, default=None):
        """
        Looks up the help text for the passed in field.
        """
        help = None

        # is there a label specified for this field
        if field in self.field_config and 'help' in self.field_config[field]:
            help = self.field_config[field]['help']

        # if we were given a default, use that
        elif default:
            help = default

        # try to see if there is a description on our model
        elif hasattr(self, 'model'):
            for model_field in self.model._meta.fields:
                if model_field.name == field:
                    help = model_field.help_text
                    break

        return help

    def lookup_field_class(self, field, obj=None, default=None):
        """
        Looks up any additional class we should include when rendering this field
        """
        css = ""

        # is there a class specified for this field
        if field in self.field_config and 'class' in self.field_config[field]:
            css = self.field_config[field]['class']

        # if we were given a default, use that
        elif default:
            css = default

        return css

    def derive_field_label(self, field, obj=None):
        """
        Derives a field label for the passed in field name.
        """
        # replace _'s with ' '
        label = field.replace('_', ' ').title()
        return label

    def derive_field_config(self):
        """
        Derives the field config for this instance.  By default we just use
        self.field_config
        """
        return self.field_config

    def get_template_names(self):
        """
        Returns the name of the template to use to render this request.

        Smartmin provides default templates as fallbacks, so appends it's own templates names to the end
        of whatever list is built by the generic views.

        Subclasses can override this by setting a 'template_name' variable on the class.
        """
        templates = []
        if getattr(self, 'template_name', None):
            templates.append(self.template_name)

        if getattr(self, 'default_template', None):
            templates.append(self.default_template)
        else:
            templates = super(SmartView, self).get_template_names()

        return templates

    def derive_fields(self):
        """
        Default implementation
        """
        fields = []
        if self.fields:
            fields.append(self.fields)

        return fields

    def derive_exclude(self):
        """
        Returns which fields we should exclude
        """
        exclude = []
        if self.exclude:
            exclude += self.exclude

        return exclude

    def derive_refresh(self):
        """
        Returns how many milliseconds before we should refresh
        """
        return self.refresh

    def get_context_data(self, **kwargs):
        """
        We supplement the normal context data by adding our fields and labels.
        """
        context = super(SmartView, self).get_context_data(**kwargs)

        # derive our field config
        self.field_config = self.derive_field_config()

        # add our fields
        self.fields = self.derive_fields()

        # build up our current parameter string, EXCLUSIVE of our page.  These
        # are used to build pagination URLs
        url_params = "?"
        order_params = ""
        for key in self.request.GET.keys():
            if key != 'page' and key != 'pjax' and (len(key) == 0 or key[0] != '_'):
                for value in self.request.GET.getlist(key):
                    url_params += "%s=%s&" % (urlquote(key), urlquote(value))
            elif key == '_order':
                order_params = "&".join(["%s=%s" % (key, _) for _ in self.request.GET.getlist(key)])

        context['url_params'] = url_params
        context['order_params'] = order_params + "&"
        context['pjax'] = self.pjax

        # set our blocks
        context['blocks'] = dict()

        # stuff it all in our context
        context['fields'] = self.fields
        context['view'] = self
        context['field_config'] = self.field_config

        context['title'] = self.derive_title()

        # and any extra context the user specified
        context.update(self.extra_context)

        # by default, our base is 'base.html', but we might be pjax
        base_template = "base.html"
        if 'pjax' in self.request.GET or 'pjax' in self.request.POST:
            base_template = "smartmin/pjax.html"

        if 'HTTP_X_PJAX' in self.request.META:
            base_template = "smartmin/pjax.html"

        context['base_template'] = base_template

        # set our refresh if we have one
        refresh = self.derive_refresh()
        if refresh:
            context['refresh'] = refresh

        return context

    def as_json(self, context):
        """
        Responsible for turning our context into an dict that can then be serialized into an
        JSON response.
        """
        raise NotImplementedError("this view can't be rendered as JSON")

    def render_to_response(self, context, **response_kwargs):
        """
        Overloaded to deal with _format arguments.
        """
        # should we try rendering as JSON?
        if '_format' in self.request.GET and self.request.GET['_format'] == 'json':
            try:
                return JsonResponse(self.as_json(context), safe=False)
            except NotImplementedError:
                pass

        # otherwise, return normally
        return super(SmartView, self).render_to_response(context)


class SmartTemplateView(SmartView, TemplateView):
    pass


def derive_single_object_url_pattern(slug_url_kwarg, path, action):
    """
    Utility function called by class methods for single object views
    """
    if slug_url_kwarg:
        return r'^%s/%s/(?P<%s>[^/]+)/$' % (path, action, slug_url_kwarg)
    else:
        return r'^%s/%s/(?P<pk>\d+)/$' % (path, action)


class SmartSingleObjectView(SmartView):
    slug_field = None
    slug_url_kwarg = None

    def get_slug_field(self):
        """
        If `slug_field` isn't specified it defaults to `slug_url_kwarg`
        """
        return self.slug_field if self.slug_field else self.slug_url_kwarg


class SmartReadView(SmartSingleObjectView, DetailView):
    default_template = 'smartmin/read.html'
    edit_button = None

    field_config = {'modified_blurb': dict(label="Modified"),
                    'created_blurb': dict(label="Created")}

    @classmethod
    def derive_url_pattern(cls, path, action):
        return derive_single_object_url_pattern(cls.slug_url_kwarg, path, action)

    def derive_queryset(self):
        return super(SmartReadView, self).get_queryset()

    def get_queryset(self):
        self.queryset = self.derive_queryset()
        return self.queryset

    def derive_title(self):
        """
        By default we just return the string representation of our object
        """
        return str(self.object)

    def derive_fields(self):
        """
        Derives our fields.  We first default to using our 'fields' variable if available,
        otherwise we figure it out from our object.
        """
        if self.fields:
            return list(self.fields)

        else:
            fields = []
            for field in self.object._meta.fields:
                fields.append(field.name)

            # only exclude?  then remove those items there
            exclude = self.derive_exclude()

            # remove any excluded fields
            fields = [field for field in fields if field not in exclude]

            return fields

    def get_modified_blurb(self, obj):
        return "%s by %s" % (obj.modified_on.strftime("%B %d, %Y at %I:%M %p"), obj.modified_by)

    def get_created_blurb(self, obj):
        return "%s by %s" % (obj.created_on.strftime("%B %d, %Y at %I:%M %p"), obj.created_by)


class SmartDeleteView(SmartSingleObjectView, DetailView, ProcessFormView):
    default_template = 'smartmin/delete_confirm.html'
    name_field = 'name'
    cancel_url = None
    redirect_url = None

    @classmethod
    def derive_url_pattern(cls, path, action):
        return derive_single_object_url_pattern(cls.slug_url_kwarg, path, action)

    def get_cancel_url(self):
        if not self.cancel_url:
            raise ImproperlyConfigured("DeleteView must define a cancel_url")

        return smart_url(self.cancel_url, self.object)

    def pre_delete(self, obj):
        # auto populate modified_by if it is present
        if self.request.user.id and self.request.user.id > 0 and hasattr(obj, 'modified_by_id'):
            obj.modified_by = self.request.user

    def post(self, request, *args, **kwargs):
        self.object = self.get_object()
        self.pre_delete(self.object)
        redirect_url = self.get_redirect_url()
        self.object.delete()

        return HttpResponseRedirect(redirect_url)

    def get_redirect_url(self, **kwargs):
        if not self.redirect_url:
            raise ImproperlyConfigured("DeleteView must define a redirect_url")

        return smart_url(self.redirect_url)

    def get_context_data(self, **kwargs):
        """ Add in the field to use for the name field """
        context = super(SmartDeleteView, self).get_context_data(**kwargs)
        context['name_field'] = self.name_field
        context['cancel_url'] = self.get_cancel_url()
        return context


class SmartListView(SmartView, ListView):
    default_template = 'smartmin/list.html'

    link_url = None
    link_fields = None
    add_button = None
    search_fields = None
    paginate_by = 25
    field_config = {'is_active': dict(label='')}
    default_order = None
    select_related = None

    @classmethod
    def derive_url_pattern(cls, path, action):
        if action == 'list':
            return r'^%s/$' % (path)
        else:
            return r'^%s/%s/$' % (path, action)

    def derive_search_fields(self):
        """
        Derives our search fields, by default just returning what was set
        """
        return self.search_fields

    def derive_title(self):
        """
        Derives our title from our list
        """
        title = super(SmartListView, self).derive_title()

        if not title:
            return force_text(self.model._meta.verbose_name_plural).title()
        else:
            return title

    def derive_link_fields(self, context):
        """
        Used to derive which fields should be linked.  This should return a set() containing
        the names of those fields which should be linkable.
        """
        if self.link_fields is not None:
            return self.link_fields

        else:
            link_fields = set()
            if self.fields:
                for field in self.fields:
                    if field != 'is_active':
                        link_fields.add(field)
                        break

        return link_fields

    def lookup_field_link(self, context, field, obj):
        """
        By default we just return /view/{{ id }}/ for the current object.
        """
        return smart_url(self.link_url, obj)

    def lookup_field_orderable(self, field):
        """
        Returns whether the passed in field is sortable or not, by default all 'raw' fields, that
        is fields that are part of the model are sortable.
        """
        try:
            self.model._meta.get_field_by_name(field)
            return True
        except Exception:
            # that field doesn't exist, so not sortable
            return False

    def get_context_data(self, **kwargs):
        """
        Add in what fields are linkable
        """
        context = super(SmartListView, self).get_context_data(**kwargs)

        # our linkable fields
        self.link_fields = self.derive_link_fields(context)

        # stuff it all in our context
        context['link_fields'] = self.link_fields

        # our search term if any
        if 'search' in self.request.GET:
            context['search'] = self.request.GET['search']

        # our ordering field if any
        order = self.derive_ordering()
        if order:
            if order[0] == '-':
                context['order'] = order[1:]
                context['order_asc'] = False
            else:
                context['order'] = order
                context['order_asc'] = True

        return context

    def derive_select_related(self):
        return self.select_related

    def derive_queryset(self, **kwargs):
        """
        Derives our queryset.
        """
        # get our parent queryset
        queryset = super(SmartListView, self).get_queryset(**kwargs)

        # apply any filtering
        search_fields = self.derive_search_fields()
        search_query = self.request.GET.get('search')
        if search_fields and search_query:
            term_queries = []
            for term in search_query.split(' '):
                field_queries = []
                for field in search_fields:
                    field_queries.append(Q(**{field: term}))
                term_queries.append(reduce(operator.or_, field_queries))

            queryset = queryset.filter(reduce(operator.and_, term_queries))

        # add any select related
        related = self.derive_select_related()
        if related:
            queryset = queryset.select_related(*related)

        # return our queryset
        return queryset

    def get_queryset(self, **kwargs):
        """
        Gets our queryset.  This takes care of filtering if there are any
        fields to filter by.
        """
        queryset = self.derive_queryset(**kwargs)

        return self.order_queryset(queryset)

    def derive_ordering(self):
        """
        Returns what field should be used for ordering (using a prepended '-' to indicate descending sort).

        If the default order of the queryset should be used, returns None
        """
        if '_order' in self.request.GET:
            return self.request.GET['_order']
        elif self.default_order:
            return self.default_order
        else:
            return None

    def order_queryset(self, queryset):
        """
        Orders the passed in queryset, returning a new queryset in response.  By default uses the _order query
        parameter.
        """
        order = self.derive_ordering()

        # if we get our order from the request
        # make sure it is a valid field in the list
        if '_order' in self.request.GET:
            if order.lstrip('-') not in self.derive_fields():
                order = None

        if order:
            # if our order is a single string, convert to a simple list
            if isinstance(order, str):
                order = (order,)

            queryset = queryset.order_by(*order)

        return queryset

    def derive_fields(self):
        """
        Derives our fields.
        """
        if self.fields:
            return self.fields

        else:
            fields = []
            for field in self.object_list.model._meta.fields:
                if field.name != 'id':
                    fields.append(field.name)
            return fields

    def get_is_active(self, obj):
        """
        Default implementation of get_is_active which returns a simple div so as to
        render a green dot for active items and nothing for inactive ones.

        Users of SmartModel will get this rendering for free.
        """
        if obj.is_active:
            return '<div class="active_icon"></div>'
        else:
            return ''

    def render_to_response(self, context, **response_kwargs):
        """
        Overloaded to deal with _format arguments.
        """
        # is this a select2 format response?
        if self.request.GET.get('_format', 'html') == 'select2':

            results = []
            for obj in context['object_list']:
                result = None
                if hasattr(obj, 'as_select2'):
                    result = obj.as_select2()

                if not result:
                    result = dict(id=obj.pk, text="%s" % obj)

                results.append(result)

            has_more = context['page_obj'].has_next() if context['page_obj'] else False

            json_data = dict(results=results, err='nil', more=has_more)
            return JsonResponse(json_data)
        # otherwise, return normally
        else:
            return super(SmartListView, self).render_to_response(context)


class SmartCsvView(SmartListView):

    def derive_filename(self):
        filename = getattr(self, 'filename', None)
        if not filename:
            filename = "%s.csv" % self.model._meta.verbose_name.lower()
        return filename

    def render_to_response(self, context, **response_kwargs):
        import csv

        # Create the HttpResponse object with the appropriate CSV header.
        response = HttpResponse(content_type='text/csv; charset=utf-8')
        response['Content-Disposition'] = 'attachment; filename=%s' % self.derive_filename()

        writer = csv.writer(response, quoting=csv.QUOTE_ALL)

        fields = self.derive_fields()

        # build up our header row
        header = []
        for field in fields:
            header.append(str(self.lookup_field_label(dict(), field)))
        writer.writerow([s.encode("utf-8") for s in header])

        # then our actual values
        for obj in self.object_list:
            row = []
            for field in fields:
                row.append(str(self.lookup_field_value(dict(), obj, field)))
            writer.writerow([s.encode("utf-8") for s in row])

        return response


class SmartXlsView(SmartListView):

    def derive_filename(self):
        filename = getattr(self, 'filename', None)
        if not filename:
            filename = "%s.xls" % self.model._meta.verbose_name.lower()
        return filename

    def render_to_response(self, context, **response_kwargs):

        from xlwt import Workbook
        book = Workbook()
        sheet1 = book.add_sheet(self.derive_title())
        fields = self.derive_fields()

        # build up our header row
        for col in range(len(fields)):
            field = fields[col]
            sheet1.write(0, col, str(self.lookup_field_label(dict(), field)))

        # then our actual values
        for row in range(len(self.object_list)):
            obj = self.object_list[row]
            for col in range(len(fields)):
                field = fields[col]
                value = str(self.lookup_field_value(dict(), obj, field))
                # skip the header
                sheet1.write(row + 1, col, value)

        # Create the HttpResponse object with the appropriate header.
        response = HttpResponse(content_type='application/vnd.ms-excel')
        response['Content-Disposition'] = 'attachment; filename=%s' % self.derive_filename()
        book.save(response)
        return response


class SmartFormMixin(object):
    readonly = ()
    field_config = {'modified_blurb': dict(label="Modified"),
                    'created_blurb': dict(label="Created")}
    success_message = None
    submit_button_name = _("Submit")

    def derive_title(self):
        """
        Derives our title from our object
        """
        if not self.title:
            return _("Form")
        else:
            return self.title

    def derive_success_message(self):
        """
        Returns a message to display when this form is successfully saved
        """
        return self.success_message

    def get_form(self):
        """
        Returns an instance of the form to be used in this view.
        """
        self.form = super(SmartFormMixin, self).get_form()

        fields = list(self.derive_fields())

        # apply our field filtering on our form class
        exclude = self.derive_exclude()
        exclude += self.derive_readonly()

        # remove any excluded fields
        for field in exclude:
            if field in self.form.fields:
                del self.form.fields[field]

        if fields is not None:
            # filter out our form fields
            remove = [name for name in self.form.fields.keys() if name not in fields]
            for name in remove:
                del self.form.fields[name]

        # stuff in our referer as the default location for where to return
        location = forms.CharField(widget=forms.widgets.HiddenInput(), required=False)

        if ('HTTP_REFERER' in self.request.META):
            location.initial = self.request.META['HTTP_REFERER']

        # add the location to our form fields
        self.form.fields['loc'] = location

        if fields:
            fields.append('loc')

        # provides a hook to programmatically customize fields before rendering
        for (name, field) in self.form.fields.items():
            field = self.customize_form_field(name, field)
            self.form.fields[name] = field

        return self.form

    def customize_form_field(self, name, field):
        """
        Allows views to customize their form fields.  By default, Smartmin replaces the plain textbox
        date input with it's own DatePicker implementation.
        """
        if isinstance(field, forms.fields.DateField) and isinstance(field.widget, forms.widgets.DateInput):
            field.widget = widgets.DatePickerWidget()
            field.input_formats = [field.widget.input_format[1]] + list(field.input_formats)

        if isinstance(field, forms.fields.ImageField) and isinstance(field.widget, forms.widgets.ClearableFileInput):
            field.widget = widgets.ImageThumbnailWidget()

        return field

    def lookup_field_label(self, context, field, default=None):
        """
        Figures out what the field label should be for the passed in field name.

        We overload this so as to use our form to see if there is label set there.  If so
        then we'll pass that as the default instead of having our parent derive
        the field from the name.
        """
        default = None

        # model forms will have meta inherited from their model that includes labels
        meta_labels = self.form._meta.labels if hasattr(self.form, "_meta") else {}

        if meta_labels and field in meta_labels:
            default = meta_labels[field]
        else:
            for form_field in self.form:
                if form_field.name == field:
                    default = form_field.label
                    break

        return super(SmartFormMixin, self).lookup_field_label(context, field, default=default)

    def lookup_field_help(self, field, default=None):
        """
        Looks up the help text for the passed in field.

        This is overloaded so that we can check whether our form has help text set
        explicitly.  If so, we will pass this as the default to our parent function.
        """
        default = None

        # model forms will have meta inherited from their model that includes help texts
        meta_help_texts = self.form._meta.help_texts if hasattr(self.form, "_meta") else {}

        if meta_help_texts and field in meta_help_texts:
            default = meta_help_texts[field]
        else:
            for form_field in self.form:
                if form_field.name == field:
                    default = form_field.help_text
                    break

        return super(SmartFormMixin, self).lookup_field_help(field, default=default)

    def derive_readonly(self):
        """
        Figures out what fields should be readonly.  We iterate our field_config to find all
        that have a readonly of true
        """
        readonly = list(self.readonly)
        for key, value in self.field_config.items():
            if 'readonly' in value and value['readonly']:
                readonly.append(key)

        return readonly

    def derive_fields(self):
        """
        Derives our fields.
        """
        if self.fields is not None:
            fields = list(self.fields)
        else:
            form = self.form
            fields = []
            for field in form:
                fields.append(field.name)

            # this is slightly confusing but we add in readonly fields here because they will still
            # need to be displayed
            readonly = self.derive_readonly()
            if readonly:
                fields += readonly

        # remove any excluded fields
        for exclude in self.derive_exclude():
            if exclude in fields:
                fields.remove(exclude)

        return fields

    def get_form_class(self):
        """
        Returns the form class to use in this view
        """
        if self.form_class:
            form_class = self.form_class

        else:
            if self.model is not None:
                # If a model has been explicitly provided, use it
                model = self.model
            elif hasattr(self, 'object') and self.object is not None:
                # If this view is operating on a single object, use
                # the class of that object
                model = self.object.__class__
            else:
                # Try to get a queryset and extract the model class
                # from that
                model = self.get_queryset().model

            # run time parameters when building our form
            factory_kwargs = self.get_factory_kwargs()
            form_class = model_forms.modelform_factory(model, **factory_kwargs)

        return form_class

    def get_factory_kwargs(self):
        """
        Let's us specify any extra parameters we might want to call for our form factory.

        These can include: 'form', 'fields', 'exclude' or 'formfield_callback'
        """
        params = dict()

        exclude = self.derive_exclude()
        exclude += self.derive_readonly()

        if self.fields:
            fields = list(self.fields)
            for ex in exclude:
                if ex in fields:
                    fields.remove(ex)

            params['fields'] = fields

        if exclude:
            params['exclude'] = exclude

        return params

    def get_success_url(self):
        """
        By default we use the referer that was stuffed in our
        form when it was created
        """
        if self.success_url:
            # if our smart url references an object, pass that in
            if self.success_url.find('@') > 0:
                return smart_url(self.success_url, self.object)
            else:
                return smart_url(self.success_url, None)

        elif 'loc' in self.form.cleaned_data:
            return self.form.cleaned_data['loc']

        raise ImproperlyConfigured("No redirect location found, override get_success_url to not use redirect urls")

    def derive_initial(self):
        """
        Returns what initial dict should be passed to our form. By default this is empty.
        """
        return dict()

    def get_form_kwargs(self):
        """
        We override this, using only those fields specified if they are specified.

        Otherwise we include all fields in a standard ModelForm.
        """
        kwargs = super(SmartFormMixin, self).get_form_kwargs()
        kwargs['initial'] = self.derive_initial()
        return kwargs

    def derive_submit_button_name(self):
        """
        Returns the name for our button
        """
        return self.submit_button_name

    def get_context_data(self, **kwargs):
        context = super(SmartFormMixin, self).get_context_data(**kwargs)
        context['submit_button_name'] = self.derive_submit_button_name()
        return context


class SmartFormView(SmartFormMixin, SmartView, FormView):
    default_template = 'smartmin/form.html'

    def form_valid(self, form):
        # plug in our success message
        messages.success(self.request, self.derive_success_message())
        return super(SmartFormView, self).form_valid(form)


class SmartModelFormView(SmartFormMixin, SmartSingleObjectView, ModelFormMixin):
    javascript_submit = None

    field_config = {'modified_blurb': dict(label="Modified"), 'created_blurb': dict(label="Created")}

    def derive_title(self):
        """
        Derives our title from our object
        """
        if not self.title:
            return _("Edit %s") % force_text(self.model._meta.verbose_name).title()
        else:
            return self.title

    def pre_save(self, obj):
        """
        Called before an object is saved away
        """
        return obj

    def save(self, obj):
        """
        Actually does the saving of this object, this is when the object is committed
        """
        self.object.save()
        self.save_m2m()

    def form_valid(self, form):
        self.object = form.save(commit=False)

        try:
            self.object = self.pre_save(self.object)
            self.save(self.object)
            self.object = self.post_save(self.object)

            messages.success(self.request, self.derive_success_message())
            if 'HTTP_X_FORMAX' not in self.request.META:
                return HttpResponseRedirect(self.get_success_url())
            else:
                response = self.render_to_response(self.get_context_data(form=form))
                response['REDIRECT'] = self.get_success_url()
                return response

        except IntegrityError as e:
            message = str(e).capitalize()
            errors = self.form._errors.setdefault(forms.forms.NON_FIELD_ERRORS, forms.utils.ErrorList())
            errors.append(message)
            return self.render_to_response(self.get_context_data(form=form))

    def save_m2m(self):
        """
        By default saves the form's m2m, can be overridden if a more complicated m2m model exists
        """
        self.form.save_m2m()

    def post_save(self, obj):
        """
        Called after an object is successfully saved
        """
        return obj

    def get_context_data(self, **kwargs):
        context = super(SmartModelFormView, self).get_context_data(**kwargs)
        context['javascript_submit'] = self.javascript_submit
        return context


class SmartUpdateView(SmartModelFormView, UpdateView):
    default_template = 'smartmin/update.html'
    exclude = ('created_by', 'modified_by')
    submit_button_name = _("Save Changes")

    # allows you to specify the name of URL to use for a remove link that will automatically be shown
    delete_url = None

    @classmethod
    def derive_url_pattern(cls, path, action):
        return derive_single_object_url_pattern(cls.slug_url_kwarg, path, action)

    def derive_queryset(self):
        return super(SmartUpdateView, self).get_queryset()

    def get_queryset(self):
        self.queryset = self.derive_queryset()
        return self.queryset

    def derive_success_message(self):
        # First check whether a default message has been set
        if self.success_message is None:
            return "Your %s has been updated." % self.model._meta.verbose_name
        else:
            return self.success_message

    def pre_save(self, obj):
        # auto populate modified_by if it is present
        if self.request.user.id and self.request.user.id > 0 and hasattr(obj, 'modified_by_id'):
            obj.modified_by = self.request.user

        return obj

    def get_context_data(self, **kwargs):
        context = super(SmartUpdateView, self).get_context_data(**kwargs)

        if self.delete_url:
            context['delete_url'] = smart_url(self.delete_url, self.object)

        return context

    def get_modified_blurb(self, obj):
        return "%s by %s" % (obj.modified_on.strftime("%B %d, %Y at %I:%M %p"), obj.modified_by)

    def get_created_blurb(self, obj):
        return "%s by %s" % (obj.created_on.strftime("%B %d, %Y at %I:%M %p"), obj.created_by)


class SmartModelActionView(SmartFormMixin, SmartSingleObjectView, DetailView, ProcessFormView):

    @classmethod
    def derive_url_pattern(cls, path, action):
        return derive_single_object_url_pattern(cls.slug_url_kwarg, path, action)

    def execute_action(self):
        """
        Subclasses should do their work here. They can throw a ValidationError to return
        control back to the Form input with said error. If no error is thrown page will
        be redirected to success_url
        """
        pass

    def post(self, request, *args, **kwargs):
        self.object = self.get_object()
        return super(SmartModelActionView, self).post(request, *args, **kwargs)

    def form_valid(self, form):
        try:
            self.execute_action()

        except forms.ValidationError as e:
            # turns out we aren't valid after all, stuff our error into our form
            self.form.add_error(None, e)
            return self.form_invalid(form)

        # all went well, stuff our success message in and return
        messages.success(self.request, self.derive_success_message())
        return super(SmartModelActionView, self).form_valid(form)


class SmartMultiFormView(SmartView, TemplateView):
    default_template = 'smartmin/multi_form.html'
    forms = {}

    # allows you to specify the name of URL to use for a remove link that will automatically be shown
    delete_url = None

    def get(self, request, *args, **kwargs):
        context = self.get_context_data(**kwargs)

        page_forms = []
        for prefix, form in self.forms.items():
            f = form(prefix=prefix)
            page_forms.append(f)

        context['forms'] = page_forms

        return self.render_to_response(context)

    def post(self, request, *args, **kwargs):
        context = self.get_context_data(**kwargs)

        # process our forms
        page_forms = []
        valid = True
        for prefix, form in self.forms.items():
            f = form(request.POST, prefix=prefix)
            valid = valid and f.is_valid()
            page_forms.append(f)

        if not valid:
            context['forms'] = page_forms
            return self.render_to_response(context)
        else:
            # redirect to success page
            pass

    def get_context_data(self, **kwargs):
        context = super(SmartMultiFormView, self).get_context_data(**kwargs)

        if self.delete_url:
            context['delete_url'] = smart_url(self.delete_url, self.object)

        return context


class SmartCreateView(SmartModelFormView, CreateView):
    default_template = 'smartmin/create.html'
    exclude = ('created_by', 'modified_by', 'is_active')
    submit_button_name = _("Create")

    def has_object_permission(self, getter_name):
        # create views don't have an object, so this is always False
        return False

    def pre_save(self, obj):
        if self.request.user.id and self.request.user.id > 0:
            # auto populate created_by if it is present
            if hasattr(obj, 'created_by_id'):
                obj.created_by = self.request.user

            # auto populate modified_by if it is present
            if hasattr(obj, 'modified_by_id'):
                obj.modified_by = self.request.user

        return obj

    def derive_success_message(self):
        # First check whether a default message has been set
        if self.success_message is None:
            return _("Your new %s has been created.") % self.model._meta.verbose_name
        else:
            return self.success_message

    def derive_title(self):
        """
        Derives our title from our object
        """
        if not self.title:
            return _("Create %s") % force_text(self.model._meta.verbose_name).title()
        else:
            return self.title


class SmartCSVImportView(NonAtomicMixin, SmartCreateView):
    success_url = 'id@csv_imports.importtask_read'

    fields = ('csv_file',)

    def derive_title(self):
        return _("Import %s") % self.crudl.model._meta.verbose_name_plural.title()

    def pre_save(self, obj):
        obj = super(SmartCSVImportView, self).pre_save(obj)
        obj.model_class = "%s.%s" % (self.crudl.model.__module__, self.crudl.model.__name__)
        return obj

    def post_save(self, task):
        task = super(SmartCSVImportView, self).post_save(task)

        task.import_params = json.dumps(self.form.data)

        # kick off our CSV import
        task.start()

        return task


class SmartCRUDL(object):
    actions = ('create', 'read', 'update', 'delete', 'list')
    model_name = None
    app_name = None
    module_name = None
    path = None

    permissions = True

    def __init__(self, model=None, path=None, actions=None):
        # set our model if passed in
        if model:
            self.model = model

        # derive our model name
        if not self.model_name:
            self.model_name = self.model._meta.object_name

        # derive our app name
        if not self.app_name:
            self.app_name = self.model._meta.app_label

        # derive our path from our class name
        if not path and not self.path:
            self.path = self.model_name.lower()

        # derive our module name from our class's module
        if not self.module_name:
            parts = self.__class__.__module__.split(".")
            self.module_name = parts[-2]

            # deal with special case of views subdirectories, we need to go up one more to find the real module
            if self.module_name == 'views' and len(parts) >= 3:
                self.module_name = parts[-3]

        # set our actions if set
        if actions:
            self.actions = actions

    def permission_for_action(self, action):
        """
        Returns the permission to use for the passed in action
        """
        return "%s.%s_%s" % (self.app_name.lower(), self.model_name.lower(), action)

    def template_for_action(self, action):
        """
        Returns the template to use for the passed in action
        """
        return "%s/%s_%s.html" % (self.module_name.lower(), self.model_name.lower(), action)

    def url_name_for_action(self, action):
        """
        Returns the reverse name for this action
        """
        return "%s.%s_%s" % (self.module_name.lower(), self.model_name.lower(), action)

    def view_for_action(self, action):
        """
        Returns the appropriate view class for the passed in action
        """
        # this turns replace_foo into ReplaceFoo and read into Read
        class_name = "".join([word.capitalize() for word in action.split("_")])
        view = None

        # see if we have a custom class defined for this action
        if hasattr(self, class_name):
            # return that one
            view = getattr(self, class_name)

            # no model set?  set it ourselves
            if not getattr(view, 'model', None):
                view.model = self.model

            # no permission and we are supposed to set them, do so
            if not hasattr(view, 'permission') and self.permissions:
                view.permission = self.permission_for_action(action)

            # set our link URL based on read and update
            if not getattr(view, 'link_url', None):
                if 'read' in self.actions:
                    view.link_url = 'id@%s' % self.url_name_for_action('read')
                elif 'update' in self.actions:
                    view.link_url = 'id@%s' % self.url_name_for_action('update')

            # if we can't infer a link URL then view class must override lookup_field_link
            if not getattr(view, 'link_url', None) and 'lookup_field_link' not in view.__dict__:
                view.link_fields = ()

            # set add_button based on existence of Create view if add_button not explicitly set
            if action == 'list' and getattr(view, 'add_button', None) is None:
                view.add_button = 'create' in self.actions

            # set edit_button based on existence of Update view if edit_button not explicitly set
            if action == 'read' and getattr(view, 'edit_button', None) is None:
                view.edit_button = 'update' in self.actions

            # if update or create, set success url if not set
            if not getattr(view, 'success_url', None) and (action == 'update' or action == 'create'):
                view.success_url = '@%s' % self.url_name_for_action('list')

        # otherwise, use our defaults
        else:
            options = dict(model=self.model)

            # if this is an update or create, and we have a list view, then set the default to that
            if action == 'update' or action == 'create' and 'list' in self.actions:
                options['success_url'] = '@%s' % self.url_name_for_action('list')

            # set permissions if appropriate
            if self.permissions:
                options['permission'] = self.permission_for_action(action)

            if action == 'create':
                view = type(str("%sCreateView" % self.model_name), (SmartCreateView,), options)

            elif action == 'read':
                if 'update' in self.actions:
                    options['edit_button'] = True

                view = type(str("%sReadView" % self.model_name), (SmartReadView,), options)

            elif action == 'update':
                if 'delete' in self.actions:
                    options['delete_url'] = 'id@%s' % self.url_name_for_action('delete')

                view = type(str("%sUpdateView" % self.model_name), (SmartUpdateView,), options)

            elif action == 'delete':
                if 'list' in self.actions:
                    options['cancel_url'] = '@%s' % self.url_name_for_action('list')
                    options['redirect_url'] = '@%s' % self.url_name_for_action('list')

                elif 'update' in self.actions:
                    options['cancel_url'] = '@%s' % self.url_name_for_action('update')

                view = type(str("%sDeleteView" % self.model_name), (SmartDeleteView,), options)

            elif action == 'list':
                if 'read' in self.actions:
                    options['link_url'] = 'id@%s' % self.url_name_for_action('read')
                elif 'update' in self.actions:
                    options['link_url'] = 'id@%s' % self.url_name_for_action('update')
                else:
                    options['link_fields'] = ()

                if 'create' in self.actions:
                    options['add_button'] = True

                view = type(str("%sListView" % self.model_name), (SmartListView,), options)

            elif action == 'csv_import':
                options['model'] = ImportTask
                view = type(str("%sCSVImportView" % self.model_name), (SmartCSVImportView,), options)

        if not view:
            # couldn't find a view?  blow up
            raise Exception("No view found for action: %s" % action)

        # set the url name for this view
        view.url_name = self.url_name_for_action(action)

        # no template set for it?  set one based on our action and app name
        if not getattr(view, 'template_name', None):
            view.template_name = self.template_for_action(action)

        view.crudl = self

        return view

    def pattern_for_view(self, view, action):
        """
        Returns the URL pattern for the passed in action.
        """
        # if this view knows how to define a URL pattern, call that
        if getattr(view, 'derive_url_pattern', None):
            return view.derive_url_pattern(self.path, action)

        # otherwise take our best guess
        else:
            return r'^%s/%s/$' % (self.path, action)

    def as_urlpatterns(self):
        """
        Creates the appropriate URLs for this object.
        """
        urls = []

        # for each of our actions
        for action in self.actions:
            view_class = self.view_for_action(action)
            view_pattern = self.pattern_for_view(view_class, action)
            name = self.url_name_for_action(action)
            urls.append(url(view_pattern, view_class.as_view(), name=name))

        return urls
