Flask-admin: Guide: how to customzie SQLA related model appearances in ModelView

Created on 6 Dec 2017  路  4Comments  路  Source: flask-admin/flask-admin

I had to go from this:

image

To this:
image

Here's how to do it.

The representation is defined by this method of flask_admin.contrib.sqla.ajax.QueryAjaxModelLoader:

class QueryAjaxModelLoader(AjaxModelLoader):
      #skipped lines
      def format(self, model):
            if not model:
                return None
            return (getattr(model, self.pk), as_unicode(model))

There is no way around subclassing a loader.
Subclass it:

class CustomFormatAjaxLoader(QueryAjaxModelLoader):
    def format(self, model):
        if not model:
            return None
        format_func = self.options['format_func']
        return (getattr(model, self.pk), format_func(model))

Notice I am passing a new function using the options dict. It's base I want to reuse the loader, and apply different formatting to different models.

Now that we have a custom loader, we need to make the ModelView use it.
ModelView has a method _create_ajax_loader that calls create_ajax_loader.
We have to override the process with a custom create_ajax_loader function.
Unfortunately the only way to do this that I found was to copy/paste the original create_ajax_loader and replace the loader class because the loader class is hardcoded.

def override_create_ajax_loader(model, session, name, field_name, options, format_func=lambda x: str(x)):
    attr = getattr(model, field_name, None)

    if attr is None:
        raise ValueError('Model %s does not have field %s.' % (model, field_name))

    if not is_relationship(attr) and not is_association_proxy(attr):
        raise ValueError('%s.%s is not a relation.' % (model, field_name))

    if is_association_proxy(attr):
        attr = attr.remote_attr

    remote_model = attr.prop.mapper.class_

    options['format_func'] = format_func
    options['order_by'] = 'name'
    return CustomFormatAjaxLoader(name, session, remote_model, **options)

I am also passing a format_func here.
order_by is hardcoded as a quick fix, without it AJAX queries trigger errors on MSSQL. You can ignore it if you are not using MSSQL.

Finally subclass ModelView and override loader creation:

def format_generation(gen):
    return 'Generation ({}) of serie {}'.format(gen.name, gen.serie.name)
class SerieModelView(ModelView):
    form_ajax_refs = {
        'generations': {
            'fields' :['id', 'name']
        },
    }

    def _create_ajax_loader(self, name, options):
        return override_create_ajax_loader(self.model, self.session, name, name, options, format_func=format_generation)

That's it. Now you control the representation using the format_func. You can pass any callable that returns a string, it should work.

Whole code:

from flask_admin.contrib.sqla.ajax import QueryAjaxModelLoader
from flask_admin.contrib.sqla.tools import is_relationship, is_association_proxy
class CustomFormatAjaxLoader(QueryAjaxModelLoader):
    def format(self, model):
        if not model:
            return None
        format_func = self.options['format_func']
        return (getattr(model, self.pk), format_func(model))

def override_create_ajax_loader(model, session, name, field_name, options, format_func=lambda x: str(x)):
    attr = getattr(model, field_name, None)

    if attr is None:
        raise ValueError('Model %s does not have field %s.' % (model, field_name))

    if not is_relationship(attr) and not is_association_proxy(attr):
        raise ValueError('%s.%s is not a relation.' % (model, field_name))

    if is_association_proxy(attr):
        attr = attr.remote_attr

    remote_model = attr.prop.mapper.class_

    options['format_func'] = format_func
    options['order_by'] = 'name'
    return CustomFormatAjaxLoader(name, session, remote_model, **options)

format_generation(gen):
    return 'Generation ({}) of serie {}'.format(gen.name, gen.serie.name)

class SerieModelView(ModelView):
    form_ajax_refs = {
        'generations': {
            'fields' :['id', 'name']
        },
    }

    def _create_ajax_loader(self, name, options):
        return override_create_ajax_loader(self.model, self.session, name, name, options, format_func=format_generation)

If you know an easier way to do it - please let me know!

I would like this whole process to be less hacky.
Suggestions:

  1. Remove loader hardcoding, add ability to pass loader class somehow.
  2. Add ability to pass a function to format output, for example via form_ajax_refs:
form_ajax_refs = {
        'generations': {
            'fields' :['id', 'name'],
            'format': lambda x: str(x),
        },
}

Most helpful comment

@Euphe
Isn't __repr__ used for programmer-readable (and also unique) representation of object?

All 4 comments

That's definitely an overkill. All you have to do as to add a __str__ property on your model (python 3, __unicode__ for python 2).

@mrjoes I have a __str__ property on my model.
The problem is my __str__ property is used elsewhere. To be precise I use it to create a string representation of models for logs.
That is my __str__ generates a _programmer-readable_ representation.
But in the admin I want to generate a _human-readable_ representation.

So my models are used not only in the flask_admin utilizing project. If I changed __str__ method of the model to fit flask_admin conventions then I would have to change all of my other projects.

It makes no sense to not be able to control the output format. It makes it impossible to tailor the representation to a single project (or single view even).

Using __str__ is a reasonable default, but I would like a way to override it.

@Euphe
Isn't __repr__ used for programmer-readable (and also unique) representation of object?

@iavael well, not really:

If at all possible, this should look like a valid Python expression that could be used to recreate an object with the same value (given an appropriate environment).

Still, this is a question of how the framework forces my models to contain the representation logic, whilist its flask-admin that should take care of representation.

Was this page helpful?
0 / 5 - 0 ratings