Multiple choices in flask-admin form_choices
Asked Answered
R

2

8

Well, I recently approached to flask-admin and I cannot figure out how to solve this issue. I know that I can use form_choices to restrict the possible values for a text-field by specifying a list of tuples. Anyway, form_choices allows to select only one value at a time. How can I specify that in some cases I may need of a comma-separated list of values?

I tried this workaround:

form_args = {
    'FIELD': {
        'render_kw': {"multiple": "multiple"},
    }
}

but, even though a multiselect input actually appears on the webpage, only the first value is saved.

EIDT 05/13/2017

By playing a bit with flask-admin I found two possible (partial-)solution for my question, both of them with specific drawbacks.

1) The first deals with the use of Select2TagsField

from flask_admin.form.fields import Select2TagsField
...
form_extra_fields = {
    'muri': Select2TagsField()
}

With this method is it possible to easily implement select menu for normal input text, even though at present I do not understand how to pass choices to Select2TagsField. It works well as a sort of multiple free text input. However, as far as I understand, it is not possible to pair Select2TagsField and form_choices

2) The second is a bit longer but it offers some more control on code (at least I presume). Still it implies the use of form_choices, but this time paired with on_model_change

form_args = {
    'FIELD': {
        'render_kw': {"multiple": "multiple"},
    }
}
form_choices = {'FIELD': [
    ('1', 'M1'), ('2', 'M2'), ('3', 'M3'), ('4', 'M4')
]}
...
def on_model_change(self, form, model, is_created):
    if len(form.FIELD.raw_data) > 1:
        model.FIELD = ','.join(form.FIELD.raw_data)

This solution, despite the former one, allows to map choices and works well when adding data to the model, but in editing it gives some problems. Any time I open the edit dialog the FIELD is empty. If I look at the data sent to the form (with on_form_prefill by printing form.FIELD.data) I get a comma separated string in the terminal but nothing appear in the pertinent select field on the webpage.

Rusell answered 23/4, 2017 at 20:57 Comment(0)
K
6

Maybe this is already outdated but I managed to change it and make it work with a multiple choice array field from postgres. To make it work I extended Select2Field to know how to deal with the list:

class MultipleSelect2Field(Select2Field):
    """Extends select2 field to make it work with postgresql arrays and using choices.

    It is far from perfect and it should be tweaked it a bit more.
    """

    def iter_choices(self):
        """Iterate over choices especially to check if one of the values is selected."""
        if self.allow_blank:
            yield (u'__None', self.blank_text, self.data is None)

        for value, label in self.choices:
            yield (value, label, self.coerce(value) in self.data)

    def process_data(self, value):
        """This is called when you create the form with existing data."""
        if value is None:
            self.data = []
        else:
            try:
                self.data = [self.coerce(value) for value in value]
            except (ValueError, TypeError):
                self.data = []

    def process_formdata(self, valuelist):
        """Process posted data."""
        if not valuelist:
            return

        if valuelist[0] == '__None':
            self.data = []
        else:
            try:
                self.data = [self.coerce(value) for value in valuelist]
            except ValueError:
                raise ValueError(self.gettext(u'Invalid Choice: could not coerce'))

    def pre_validate(self, form):
        """Validate sent keys to make sure user don't post data that is not a valid choice."""
        sent_data = set(self.data)
        valid_data = {k for k, _ in self.choices}
        invalid_keys = sent_data - valid_data
        if invalid_keys:
            raise ValueError('These values are invalid {}'.format(','.join(invalid_keys)))

and to use it do this on the ModelView

class SomeView(ModelView):
    form_args = dict(FIELD=dict(render_kw=dict(multiple="multiple"), choices=CHOICES_TUPLE))
    form_overrides = dict(FIELD=MultipleSelect2Field)
Kolnick answered 4/10, 2017 at 14:33 Comment(0)
Q
4

For this approach to work you would need to use a column that can store a list of elements. At least with sqlite this is not possible using Flask-Admin. However it would be better for you to store your choices in a separate data model and use constraints to link the two models. See a working example here.

from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_admin import Admin
from flask_admin.contrib.sqla import ModelView


app = Flask(__name__)
app.config['SECRET_KEY'] = '8e12c91677b3b3df266a770b22c82f2f'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///:memory:'
db = SQLAlchemy(app)
admin = Admin(app)


item_tag_relation = db.Table('item_tag_relation',
    db.Column('item_id', db.Integer, db.ForeignKey('item.id')),
    db.Column('tag_id', db.Integer, db.ForeignKey('tag.id'))
)


class Item(db.Model):
    id = db.Column(db.Integer(), primary_key=True)
    name = db.Column(db.String())
    tags = db.relationship("Tag",
                               secondary=item_tag_relation,
                               backref="items")

    def __repr__(self):
        return self.name


class Tag(db.Model):
    id = db.Column(db.Integer(), primary_key=True)
    name = db.Column(db.String())

    def __repr__(self):
        return self.name


class ItemModelView(ModelView):
    pass


db.create_all()
admin.add_view(ItemModelView(Item, db.session))
admin.add_view(ModelView(Tag, db.session))

if __name__ == '__main__':
    app.run(debug=True)
Quattlebaum answered 24/4, 2017 at 10:8 Comment(1)
thanks MrLeeh for you answer (the right one, no doubt). I know that the proposed approach indeed is not very orthodox but, since in my model there are many case such the above mentioned one (however, cases that must be used very seldom), I was wondering if there are some methods to inherit the same behaviour of a N:M relationship without alterning the model. I was thinking to a comma separated list of string, but I don't know how to specify this with flask-adminRusell

© 2022 - 2024 — McMap. All rights reserved.