question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Guide: how to customzie SQLA related model appearances in ModelView

See original GitHub issue

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),
        },
}

Issue Analytics

  • State:open
  • Created 6 years ago
  • Comments:5 (2 by maintainers)

github_iconTop GitHub Comments

4reactions
btseytlincommented, Dec 7, 2017

@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.

3reactions
iavaelcommented, Jan 9, 2018

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

Read more comments on GitHub >

github_iconTop Results From Across the Web

MySQL Workbench Manual :: 3.2.4 Modeling Preferences
Preferences: Modeling: MySQL. This preference group enables you to set model-related options specific to your MySQL version (see the figure that follows).
Read more >
Data Modeler Concepts and Usage - Oracle Help Center
For example, for a relational model the icons include New Table, New View, Split Table, Merge Tables, New FK Relation, Generate DDL, Synchronize...
Read more >
Query Processing Architecture Guide - SQL Server
How SQL Server processes queries and optimizes query reuse through execution plan caching.
Read more >
Performing raw SQL queries | Django documentation
raw() to perform raw queries and return model instances, or you can avoid the model layer entirely and execute custom SQL directly. Explore...
Read more >
Using SQL Runner to create queries and Explores | Looker
Select the model you want to query. Click in the SQL Query area and enter your SQL query using LookML fields. Optionally, double-click...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found