Search code examples
pythonpython-3.xflaskflask-wtformsflask-admin

How to reorder fields in a Flask-Admin form using an Ajax selector with scaffold_form method?


I created a custom Ajax selector in a Flask-Admin view to optimize search in the birth_city_id field (over 36000 items) using form_ajax_refs (based on How to do an Ajax form field on Flask Admin implementation). While the Ajax selector works correctly, I have an issue with how fields are displayed in the form.

The birth_city_id field only appears if I add it explicitly in the scaffold_form method (I tried with form_overrides attribute too, but it's fail). However, this results in the field being placed at the end of the form, whereas it should appear before personal_information and professional_information fields.

Here’s my code:

from flask_admin.model.fields import AjaxSelectField
from flask_admin.model.ajax import AjaxModelLoader, DEFAULT_PAGE_SIZE

from ..models import *

class CityAjaxModelLoader(AjaxModelLoader):
    def __init__(self, name, **options):
        super(CityAjaxModelLoader, self).__init__(name, options)

    def format(self, model):
        if model is None:
            return None
        return (model.id, repr(model))

    def get_one(self, pk):
        return session.query(City).get(pk)

    def get_list(self, query, offset=0, limit=DEFAULT_PAGE_SIZE):
        search_term = query.strip().lower()
        return session.query(City).filter(City.label.ilike(f"%{search_term}%")).offset(offset).limit(limit).all()

class PrinterView(GlobalModelView):
    
    # ...

    form_columns = [
        "lastname",
        "firstnames",
        "birth_date",
        "birth_city_label",
        "birth_city_id",
        "personal_information",
        "professional_information",
    ]

    form_ajax_refs = {
        'birth_city_id': CityAjaxModelLoader('birth_city_id')
    }

    def scaffold_form(self):
        form_class = super(PrinterView, self).scaffold_form()
        form_class.birth_city_id = AjaxSelectField(
            CityAjaxModelLoader('birth_city_id'),
            label='Birth City (reference)',
            blank_text="Select a city..."
        )
        # reorder fields ?
        return form_class

The birth_city_id field is always placed at the end of the form, even if I explicitly include it in the form_columns list (I expected to force the fields position like this but it's change nothing).

I would like to know:

  1. Is there a way to reorder fields through scaffold_form method?

  2. Is there a better strategy to integrate an Ajax selector while maintaining the field order in the form?

Thanks in advance for your help!

[Update 1]

My Person model (linked to PrinterView) does have a relationship birth_cities linked to the birth_city_id foreign key. Here is the relevant part of the model definition:

class Person(AbstractVersion):
    __tablename__ = "persons"
    id = Column(Integer, primary_key=True, autoincrement=True, nullable=False, unique=True)
    birth_city_id = Column(Integer, ForeignKey("cities.id"), nullable=True, unique=False)

    # Relations
    birth_cities = relationship("City", foreign_keys=[birth_city_id], back_populates="persons")


class City(AbstractVersion):
    __tablename__ = "cities"
    id = Column(Integer, primary_key=True, autoincrement=True, nullable=False, unique=True)

    # Relations
    persons = relationship("Person", back_populates="birth_cities")

Solution

  • Use the relation in the form_columns definition and not the foreign key. Also, you can use the Flask-Admin supplied QueryAjaxModelLoader to do your Ajax lookups. Something like the following (untested):

    from flask_admin.contrib.sqla.ajax import QueryAjaxModelLoader
    
    class PrinterView(GlobalModelView):
        # ...
    
        form_columns = (
            "lastname",
            "firstnames",
            "birth_date",
            "birth_cities",
            "personal_information",
            "professional_information",
        )
    
        form_ajax_refs = {
            'birth_cities':  QueryAjaxModelLoader(
                'birth_cities',
                db.session,
                City,
                fields=['label'],
            ),
        }