Source code for flask_superadmin.model.base

import math
import re

from wtforms import fields, widgets
from flask import request, url_for, redirect, flash, abort

from flask_superadmin.babel import gettext
from flask_superadmin.base import BaseView, expose
from flask_superadmin.form import (BaseForm, ChosenSelectWidget, FileField,
                                   DatePickerWidget, DateTimePickerWidget)

import traceback


class AdminModelConverter(object):
    def convert(self, *args, **kwargs):
        field = super(AdminModelConverter, self).convert(*args, **kwargs)
        if field:
            widget = field.kwargs.get('widget', field.field_class.widget)
            if isinstance(widget, widgets.Select):
                field.kwargs['widget'] = ChosenSelectWidget(
                    multiple=widget.multiple)
            elif issubclass(field.field_class, fields.DateTimeField):
                field.kwargs['widget'] = DateTimePickerWidget()
            elif issubclass(field.field_class, fields.DateField):
                field.kwargs['widget'] = DatePickerWidget()
            elif issubclass(field.field_class, fields.FileField):
                field.field_class = FileField
        return field


first_cap_re = re.compile('(.)([A-Z][a-z]+)')


def camelcase_to_space(name):
    return first_cap_re.sub(r'\1 \2', name)


def prettify(str):
    return str.replace('_', ' ').title()


class BaseModelAdmin(BaseView):
    """ BaseModelAdmin provides create/edit/delete functionality for an
    abstract Model. The abstraction is further customized by the
    backend-specific model admin (see flask_superadmin/model/backends/) and
    by the user-defined admin classes inheriting from ModelAdmin.
    """

    # Number of objects to display per page in the list view
    list_per_page = 20

    # Columns to display in the list index - can be field names or callables.
    # Admin's methods have higher priority than the fields/methods on
    # the model or document.
    list_display = tuple()

    # Only fields with names specified in `fields` will be displayed in the
    # form (minus the ones mentioned in `exclude`). The order is preserved,
    # too. You can also include methods that are on the model admin, or on the
    # model/document, as long as they are marked as read-only (i.e. included
    # in `readonly_fields`). Priority of fields' lookup: methods on the model
    # admin, methods/fields on the model/document.
    fields = tuple()
    readonly_fields = tuple()
    exclude = tuple()

    # A base class for form rendering. If `None`, `BaseForm` would be used.
    form = None

    can_edit = True
    can_create = True
    can_delete = True

    list_template = 'admin/model/list.html'
    edit_template = 'admin/model/edit.html'
    add_template = 'admin/model/add.html'
    delete_template = 'admin/model/delete.html'

    search_fields = tuple()

    field_overrides = {}

    # A dictionary of field_name: overridden_params_dict, e.g.
    #   { 'name': { 'label': 'Name', 'description': 'This is a name' } }
    # Parameters that can be overridden: label, description, validators,
    # filters, default
    field_args = None

    @staticmethod
    def model_detect(model):
        return False

    def __init__(self, model=None, name=None, category=None, endpoint=None,
                 url=None):
        if name is None:
            name = '%s' % camelcase_to_space(model.__name__)

        if endpoint is None:
            endpoint = ('%s' % model.__name__).lower()

        super(BaseModelAdmin, self).__init__(name, category, endpoint, url)

        if model:
            self.model = model

    def get_display_name(self):
        return self.model.__name__

    def allow_pk(self):
        return not self.model._meta.auto_increment

    def get_column(self, instance, name):
        parts = name.split('.')
        value = instance
        for p in parts:

            # admin's methods have higher priority than the fields/methods on
            # the model or document. If a callable is found on the admin
            # level, it's also passed an instance object
            if hasattr(self, p) and callable(getattr(self, p)):
                value = getattr(self, p)(instance)
            else:
                value = getattr(value, p, None)
                if callable(value):
                    value = value()

            if not value:
                break

        return value

    def get_reference(self, column_value):
        for model, model_view in self.admin._models:
            if type(column_value) == model:
                return '/admin/%s/%s/' % (model_view.endpoint,
                                          model_view.get_pk(column_value))

    def get_readonly_fields(self, instance):
        ret_vals = {}
        if not instance:
            return ret_vals
        for field in self.readonly_fields:
            self_field = getattr(self, field, None)
            if callable(self_field):
                val = self_field(instance)
            else:
                val = getattr(instance, field)
                if callable(val):
                    val = val()
            if not isinstance(val, dict):
                # Check if the value is a reference field to a doc/model
                # registered in the admin. If so, link to it.
                reference = self.get_reference(val)
                val = {
                    'label': prettify(field),
                    'value': val,
                    'url': reference if reference else None
                }
            ret_vals[field] = val
        return ret_vals

    def get_converter(self):
        raise NotImplemented()

    def get_model_form(self):
        """ Returns the model form, should get overridden in backend-specific
        view.
        """
        raise NotImplemented()

    def get_form(self):
        model_form = self.get_model_form()
        converter = self.get_converter()
        if isinstance(converter, type):
            converter = converter()
        base_class = self.form or BaseForm
        form = model_form(self.model, base_class=base_class,
                          fields=self.fields,
                          readonly_fields=self.readonly_fields,
                          exclude=self.exclude, field_args=self.field_args,
                          converter=converter)
        return form

    def get_add_form(self):
        return self.get_form()

    def get_objects(self, *pks):
        raise NotImplemented()

    def get_object(self, pk):
        raise NotImplemented()

    def get_pk(self, instance):
        return

    def save_model(self, instance, form, adding=False):
        raise NotImplemented()

    def delete_models(self, *pks):
        raise NotImplemented()

    def is_sortable(self, column):
        return False

    def field_name(self, field):
        return prettify(field)

    def construct_search(self, field_name):
        raise NotImplemented()

    def get_queryset(self):
        raise NotImplemented()

    def get_list(self):
        raise NotImplemented()

    def get_url_name(self, name):
        URLS = {
            'index': '.list',
            'add': '.add',
            'delete': '.delete',
            'edit': '.edit'
        }
        return URLS[name]

    def dispatch_save_redirect(self, instance):
        if '_edit' in request.form:
            return redirect(
                url_for(self.get_url_name('edit'), pk=self.get_pk(instance))
            )
        elif '_add_another' in request.form:
            return redirect(url_for(self.get_url_name('add')))
        else:
            return redirect(url_for(self.get_url_name('index')))

    @expose('/add/', methods=('GET', 'POST'))
    def add(self):
        if not self.can_create:
            abort(403)

        Form = self.get_add_form()
        if request.method == 'POST':
            form = Form()
            if form.validate_on_submit():
                try:
                    instance = self.save_model(self.model(), form, adding=True)
                    flash(gettext('New %(model)s saved successfully',
                          model=self.get_display_name()), 'success')
                    return self.dispatch_save_redirect(instance)
                except Exception, ex:
                    print traceback.format_exc()
                    if hasattr(self, 'session'):
                        self.session.rollback()
                    flash(gettext('Failed to add model. %(error)s',
                          error=str(ex)), 'error')

        else:
            try:
                form = Form(obj=self.model())
            except TypeError:
                raise Exception('The database model for %r should have an '
                                '__init__ with all arguments set to defaults.'
                                % self.model.__name__)

        return self.render(self.add_template, model=self.model, form=form)

    @property
    def page(self):
        return request.args.get('page', 0, type=int)

    def total_pages(self, count):
        return int(math.ceil(float(count) / self.list_per_page))

    @property
    def sort(self):
        sort = request.args.get('sort', None)
        if sort and sort.startswith('-'):
            desc = True
            sort = sort[1:]
        else:
            desc = False
        return sort, desc

    @property
    def search(self):
        return request.args.get('q', None)

    def page_url(self, page):
        search_query = self.search
        sort, desc = self.sort
        if sort and desc:
            sort = '-' + sort
        if page == 0:
            page = None
        return url_for(self.get_url_name('index'), page=page, sort=sort,
                       q=search_query)

    def sort_url(self, sort, desc=None):
        if sort and desc:
            sort = '-' + sort
        search_query = self.search
        return url_for(self.get_url_name('index'), sort=sort, q=search_query)

    @expose('/', methods=('GET', 'POST',))
    def list(self):
        """
            List view
        """
        # Grab parameters from URL
        if request.method == 'POST':
            id_list = request.form.getlist('_selected_action')
            if id_list and (request.form.get('action-delete') or
                            request.form.get('action', None) == 'delete'):
                return self.delete(*id_list)

        sort, sort_desc = self.sort
        page = self.page
        search_query = self.search
        count, data = self.get_list(page=page, sort=sort, sort_desc=sort_desc,
                                    search_query=search_query)

        return self.render(self.list_template, data=data, page=page,
                           total_pages=self.total_pages(count), sort=sort,
                           sort_desc=sort_desc, count=count, modeladmin=self,
                           search_query=search_query)

    @expose('/<pk>/', methods=('GET', 'POST'))
    def edit(self, pk):
        try:
            instance = self.get_object(pk)
        except self.model.DoesNotExist:
            abort(404)

        Form = self.get_form()

        if request.method == 'POST':
            form = Form(obj=instance)
            if form.validate_on_submit():
                try:
                    self.save_model(instance, form, adding=False)
                    flash(
                        'Changes to %s saved successfully' % self.get_display_name(),
                        'success'
                    )
                    return self.dispatch_save_redirect(instance)
                except Exception, ex:
                    print traceback.format_exc()
                    flash(gettext('Failed to edit model. %(error)s',
                                  error=str(ex)), 'error')
        else:
            form = Form(obj=instance)

        return self.render(self.edit_template, model=self.model, form=form,
                           pk=self.get_pk(instance), instance=instance)

    @expose('/<pk>/delete/', methods=('GET', 'POST'))
    def delete(self, pk=None, *pks):
        if not self.can_delete:
            abort(403)

        if pk:
            pks += pk,

        if request.method == 'POST' and 'confirm_delete' in request.form:
            count = len(pks)
            self.delete_models(*pks)

            flash(
                'Successfully deleted %s %ss' % (count, self.get_display_name()),
                'success'
            )
            return redirect(url_for(self.get_url_name('index')))
        else:
            instances = self.get_objects(*pks)

        return self.render(self.delete_template, instances=instances)


class ModelAdmin(BaseModelAdmin):
    pass