Select2 field implementation in flask/flask-admin

2019-04-07 13:19发布

问题:

I'm trying to implement Select2 field in one of my flask views. Basically I want the same select2 field in my flask application view (not a flask admin modelview) as in Flask-admin model create views. Currently my solution has been QuerySelectField from wtforms that looks something like this

class TestForm(Form):
    name= QuerySelectField(query_factory=lambda: models.User.query.all())

This allows me to load and select all the data I need, but it does not provide select2 search box etc. Currently all that I have found is Select2Field and Select2Widget from flask/admin/form/fields and flask/admin/form/widgets similarly like in this post https://stackoverflow.com/questions/24644960/how-to-steal-flask-admin-tag-form-field and also select2 documentation at http://ivaynberg.github.io/select2/ As I understand these could be reusable, meaning there is no need for other custom widgets, custom fields.

Would be grateful if someone could provide more information about select2 field implementation in flask application (including views, templates, forms files and how to correctly "connect" with neccessary js and css files, also how to load the field up with the database model I need).

回答1:

This worked for me:

...
from wtforms.ext.sqlalchemy.fields import QuerySelectField
from flask_admin.form.widgets import Select2Widget
...

class TestForm(Form):
    name= QuerySelectField(query_factory=lambda: models.User.query.all(),
                           widget=Select2Widget())

And in your template:

{% extends "admin/master.html" %}
{% import 'admin/lib.html' as lib with context %}

{% block head %}
    {{ super() }}
    {{ lib.form_css() }}
{% endblock %}

{% block body %}
...
{% endblock %}

{% block tail %}
    {{ super() }}
    {{ lib.form_js() }}
{% endblock %}

I can try to put together a minimal working example if necessary.



回答2:

I had a similar requirement and have put together a minimal example.

Note the following:

Class TestView defines three routes; a get view, a post view and an Ajax lookup view.

Function get_loader_by_name takes a string name and returns a QueryAjaxModelLoader. This function is used in both the Ajax lookup call and in the TestForm field definitions.

The text displayed in the Select2 widgets is the value returned by the User model's __unicode__ method.

I've used Faker to generate the sample user data.

File app.py:

from flask import Flask, render_template, url_for, request, abort, json, Response
from flask.ext.admin import Admin, BaseView, expose, babel
from flask.ext.admin.contrib.sqla.ajax import QueryAjaxModelLoader
from flask.ext.admin.model.fields import AjaxSelectField, AjaxSelectMultipleField
from flask.ext.sqlalchemy import SQLAlchemy
from wtforms import Form
from faker import Factory

app = Flask(__name__)
app.config['DEBUG'] = True
app.config['SECRET_KEY'] = 'super-secret'

app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///:memory:'
app.config['SQLALCHEMY_ECHO'] = True
db = SQLAlchemy(app)

try:
    from flask_debugtoolbar import DebugToolbarExtension
    DebugToolbarExtension(app)
except:
    pass


class User(db.Model):
    __tablename__ = 'users'

    id = db.Column(db.Integer, primary_key=True)

    first_name = db.Column(db.Unicode(length=255), nullable=False)
    last_name = db.Column(db.Unicode(length=255), nullable=False)
    email = db.Column(db.Unicode(length=254), nullable=False, unique=True)

    def __unicode__(self):
        return u"{first} {last}; {email}".format(first=self.first_name, last=self.last_name, email=self.email)


def get_loader_by_name(name):
    _dicts = {
        'user': QueryAjaxModelLoader(
            'user',
            db.session, User,
            fields=['first_name', 'last_name', 'email'],
            page_size=10,
            placeholder="Select a user"
        )
    }
    return _dicts.get(name, None)


class TestView(BaseView):

    def __init__(self, name=None, category=None,
                 endpoint=None, url=None,
                 template='admin/index.html',
                 menu_class_name=None,
                 menu_icon_type=None,
                 menu_icon_value=None):
        super(TestView, self).__init__(name or babel.lazy_gettext('Home'),
                                             category,
                                             endpoint or 'admin',
                                             url or '/admin',
                                             'static',
                                             menu_class_name=menu_class_name,
                                             menu_icon_type=menu_icon_type,
                                             menu_icon_value=menu_icon_value)
        self._template = template

    @expose('/', methods=('GET',))
    def index_view(self):
        _form = TestForm()
        return self.render(self._template, form=_form)

    @expose('/', methods=('POST',))
    def post_view(self):
        pass

    @expose('/ajax/lookup/')
    def ajax_lookup(self):
        name = request.args.get('name')
        query = request.args.get('query')
        offset = request.args.get('offset', type=int)
        limit = request.args.get('limit', 10, type=int)

        loader = get_loader_by_name(name)

        if not loader:
            abort(404)

        data = [loader.format(m) for m in loader.get_list(query, offset, limit)]
        return Response(json.dumps(data), mimetype='application/json')

# Create admin and Test View
admin = Admin(app, name='Admin', template_mode='bootstrap3')
admin.add_view(TestView(template='test.html', name="Test", url='/test', endpoint='test'))


class TestForm(Form):

    single_user = AjaxSelectField(
        loader=get_loader_by_name('user')
    )

    multiple_users = AjaxSelectMultipleField(
        loader=get_loader_by_name('user')
    )


@app.route('/')
def index():
    return render_template("index.html", link=url_for('test.index_view'))


def build_db():

    db.drop_all()
    db.create_all()
    fake = Factory.create()
    for index in range(0, 1000):
        _first_name = fake.first_name()
        _last_name = fake.last_name()
        _user_db = User(
            first_name=_first_name,
            last_name=_last_name,
            email="{first}.{last}{index}@example.com".format(first=_first_name.lower(), last=_last_name.lower(), index=index)
        )
        db.session.add(_user_db)

    db.session.commit()


@app.before_first_request
def before_first_request():
    build_db()


if __name__ == '__main__':
    app.debug = True
    app.run(port=5000, debug=True)

File templates/index.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Test Select2</title>
</head>
<body>
    <a href="{{ link }}">Go to the test form</a>
</body>
</html>

File templates/test.html:

{% import 'admin/static.html' as admin_static with context %}
{% import 'admin/lib.html' as lib with context %}

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Test Select2</title>
    <link href="{{ admin_static.url(filename='bootstrap/bootstrap3/css/bootstrap.min.css') }}" rel="stylesheet">
    <link href="{{ admin_static.url(filename='bootstrap/bootstrap3/css/bootstrap-theme.min.css') }}" rel="stylesheet">
    <link href="{{ admin_static.url(filename='admin/css/bootstrap3/admin.css') }}" rel="stylesheet">
    {{ lib.form_css() }}
</head>
<body>

    <div class="container">
        <div class="row">
            <div class="col-sm-10 col-sm-offset-2">
                <form>
                    {{ lib.render_form_fields(form) }}
                </form>
            </div>
        </div>
    </div>

    <script src="{{ admin_static.url(filename='vendor/jquery-2.1.1.min.js') }}" type="text/javascript"></script>
    <script src="{{ admin_static.url(filename='bootstrap/bootstrap3/js/bootstrap.min.js') }}" type="text/javascript"></script>
    <script src="{{ admin_static.url(filename='vendor/moment-2.8.4.min.js') }}" type="text/javascript"></script>
    <script src="{{ admin_static.url(filename='vendor/select2/select2.min.js') }}" type="text/javascript"></script>
    {{ lib.form_js() }}

</body>
</html>

Update July 2018

Added a standalone Flask extension available on Github - Flask-Select2 - WIP.



回答3:

I recently implemented a "tags" field in the front-end of a Flask app, using Select2 and WTForms. I wrote a sample app, demonstrating how I got it working (the view code for populating the select options, and for dynamically saving new options, is where most of the work happens):

https://github.com/Jaza/flasktaggingtest

You can see a demo of this app at:

https://flasktaggingtest.herokuapp.com/

No AJAX autocomplete in my sample (it just populates the select field with all available tags, when initializing the form). Other than that, should be everything that you'd generally want for a tagging widget in your Flask views / templates.