форма flask-admin: ограничить значение поля 2 в зависимости от значения поля 1

одна функция, которую я изо всех сил пытаюсь реализовать в flask-admin, - это когда пользователь редактирует форму, чтобы ограничить значение поля 2 после установки Поля 1.

позвольте мне привести упрощенный пример в словах (фактический случай использования более запутан). Затем я покажу полную суть, которая реализует этот пример, минус функция "constrain".

Допустим у нас есть база данных, которая отслеживает некоторые программы "рецепты" для вывода отчетов в различных форматах. The recipe таблица из нашей базы образцов есть два рецепта: "серьезный отчет", "ASCII Art".

чтобы реализовать каждый рецепт, мы выбираем один из нескольких методов. The method таблица нашей базы данных имеет два метода:" tabulate_results","pretty_print".

каждый метод имеет свои параметры. The methodarg таблица имеет два имени параметров для "tabulate_results" ("строки", "display_total") и два параметра для"pretty_print" ("embellishment_character", "lines_to_jump").

теперь для каждого из рецепты ("серьезный отчет", "ASCII Art") нам необходимо предоставить значение аргументов их соответствующих методов ("tabulate_results", "pretty_print").

для каждой записи recipearg таблица позволяет выбрать рецепт (это поле 1, например "серьезный отчет") и имя аргумента (это поле 2). Проблема в том, что отображаются все возможные имена аргументов, тогда как они должны быть ограничены на основе значения поля 1.

что фильтрация / стесняя механизм можем ли мы реализовать так, что как только мы выберем "серьезный отчет", мы знаем, что будем использовать метод" tabulate_results", так что доступны только аргументы" rows "и" display_total"?

Я думаю, что какое-то волшебство AJAX, которое проверяет поле 1 и задает запрос для значений поля 2, но понятия не имеет, как действовать.

вы можете увидеть это, играя с сутью: нажмите на Recipe Arg tab. В первой строке ("серьезный отчет") при попытке изменить значение " Methodarg при нажатии на него доступны все четыре имени аргументов, а не только два.

# full gist: please run this

from flask import Flask
from flask_admin import Admin
from flask_admin.contrib import sqla
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy import Column, ForeignKey, Integer, String
from sqlalchemy.orm import relationship

# Create application
app = Flask(__name__)

# Create dummy secrey key so we can use sessions
app.config['SECRET_KEY'] = '123456790'

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

# Create admin app
admin = Admin(app, name="Constrain Values", template_mode='bootstrap3')

# Flask views
@app.route('/')
def index():
    return '<a href="/admin/">Click me to get to Admin!</a>'


class Method(db.Model):
    __tablename__ = 'method'
    mid = Column(Integer, primary_key=True)
    method = Column(String(20), nullable=False, unique=True)
    methodarg = relationship('MethodArg', backref='method')
    recipe = relationship('Recipe', backref='method')


    def __str__(self):
        return self.method


class MethodArg(db.Model):
    __tablename__ = 'methodarg'
    maid = Column(Integer, primary_key=True)
    mid = Column(ForeignKey('method.mid', ondelete='CASCADE', onupdate='CASCADE'), nullable=False)
    methodarg = Column(String(20), nullable=False, unique=True)
    recipearg = relationship('RecipeArg', backref='methodarg')
    inline_models = (Method,)


    def __str__(self):
        return self.methodarg


class Recipe(db.Model):
    __tablename__ = 'recipe'
    rid = Column(Integer, primary_key=True)
    mid = Column(ForeignKey('method.mid', ondelete='CASCADE', onupdate='CASCADE'), nullable=False)
    recipe = Column(String(20), nullable=False, index=True)
    recipearg = relationship('RecipeArg', backref='recipe')
    inline_models = (Method,)

    def __str__(self):
        return self.recipe


class RecipeArg(db.Model):
    __tablename__ = 'recipearg'

    raid = Column(Integer, primary_key=True)
    rid = Column(ForeignKey('recipe.rid', ondelete='CASCADE', onupdate='CASCADE'), nullable=False)
    maid = Column(ForeignKey('methodarg.maid', ondelete='CASCADE', onupdate='CASCADE'), nullable=False)
    strvalue = Column(String(80), nullable=False)
    inline_models = (Recipe, MethodArg)


    def __str__(self):
        return self.strvalue


class MethodArgAdmin(sqla.ModelView):
    column_list = ('method', 'methodarg')
    column_editable_list = column_list



class RecipeAdmin(sqla.ModelView):
    column_list = ('recipe', 'method')
    column_editable_list = column_list



class RecipeArgAdmin(sqla.ModelView):
    column_list = ('recipe', 'methodarg', 'strvalue')
    column_editable_list = column_list


admin.add_view(RecipeArgAdmin(RecipeArg, db.session))

# More submenu
admin.add_view(sqla.ModelView(Method, db.session, category='See Other Tables'))
admin.add_view(MethodArgAdmin(MethodArg, db.session, category='See Other Tables'))
admin.add_view(RecipeAdmin(Recipe, db.session, category='See Other Tables'))


if __name__ == '__main__':

    db.drop_all()
    db.create_all()
    db.session.add(Method(mid=1, method='tabulate_results'))
    db.session.add(Method(mid=2, method='pretty_print'))
    db.session.commit()
    db.session.add(MethodArg(maid=1, mid=1, methodarg='rows'))
    db.session.add(MethodArg(maid=2, mid=1, methodarg='display_total'))
    db.session.add(MethodArg(maid=3, mid=2, methodarg='embellishment_character'))
    db.session.add(MethodArg(maid=4, mid=2, methodarg='lines_to_jump'))
    db.session.add(Recipe(rid=1, mid=1, recipe='Serious Report'))
    db.session.add(Recipe(rid=2, mid=2, recipe='ASCII Art'))
    db.session.commit()
    db.session.add(RecipeArg(raid=1, rid=1, maid=2, strvalue='true' ))
    db.session.add(RecipeArg(raid=2, rid=1, maid=1, strvalue='12' ))
    db.session.add(RecipeArg(raid=3, rid=2, maid=4, strvalue='3' ))
    db.session.commit()

    # Start app
    app.run(debug=True)

1 ответов


я вижу два способа решения этой проблемы:

1-Когда Flask-Admin генерирует форму, добавьте data атрибуты mid каждого methodArg в каждом option в теге methodArg выбрать. Тогда есть некоторый фильтр кода JS option теги на основе выбранного рецепта.

редактировать

примерный попробовать на data-mid атрибут каждого option:

def monkeypatched_call(self, field, **kwargs):
    kwargs.setdefault('id', field.id)
    if self.multiple:
        kwargs['multiple'] = True
    html = ['<select %s>' % html_params(name=field.name, **kwargs)]
    for (val, label, selected), (_, methodarg) in zip(field.iter_choices(), field._get_object_list()):
        html.append(self.render_option(val, label, selected, **{'data-mid': methodarg.mid}))
    html.append('</select>')
    return HTMLString(''.join(html))

Select.__call__ = monkeypatched_call

блокатор заключается в том, что вызовы рендеринга запускаются из шаблонов jinja, поэтому вы в значительной степени застряли в обновлении виджета (Select будучи самым низким уровнем в WTForms, и используется в качестве базы для колбы-администратора Select2Field).

после получения этих data-mid на каждом из ваших вариантов вы можете продолжить просто привязку change на выбор рецепта и отображения methodarg в option, что соответствует data-mid. Учитывая колбу-админ использует select2, возможно, вам придется сделать некоторые настройки JS (самым простым уродливым решением было бы очистить виджет и воссоздать его для каждого change событие срабатывает)

в целом, я нахожу это менее надежным, чем второе решение. Я сохранил monkeypatch, чтобы дать понять, что это не должно использоваться в производстве imho. (второе решение немного менее навязчивое)

2-Используйте поддерживаемое ajax-завершение в Flask-Admin, чтобы взломать свой путь к получению параметров, которые вы хотите на основе выбранного рецепт:

сначала создайте пользовательский AjaxModelLoader, который будет отвечать за выполнение запроса правильного выбора в DB:

class MethodArgAjaxModelLoader(sqla.ajax.QueryAjaxModelLoader):
    def get_list(self, term, offset=0, limit=10):
        query = self.session.query(self.model).filter_by(mid=term)
        return query.offset(offset).limit(limit).all()

class RecipeArgAdmin(sqla.ModelView):
    column_list = ('recipe', 'methodarg', 'strvalue')
    form_ajax_refs = {
        'methodarg': MethodArgAjaxModelLoader('methodarg', db.session, MethodArg, fields=['methodarg'])
    }
    column_editable_list = column_list

затем обновите Flask-Admin's form.js чтобы получить браузер, чтобы отправить вам информацию о рецепте вместо methodArg имя, который должен быть завершаются автоматически. (или вы можете отправить оба query и сделайте некоторый анализ arg в вашем AjaxLoader, так как Flask-Admin не делает никакого анализа на query, ожидая, что это будет строка I предположим [0]. Таким образом, вы бы сохранили автозаполнение)

data: function(term, page) {
    return {
        query: $('#recipe').val(),
        offset: (page - 1) * 10,
        limit: 10
    };
},

этот фрагмент взят из Flask-Admin's form.js [1]

очевидно, что это требует некоторой настройки и параметризации (потому что такое хакерское решение блокирует использование другого ajax-заполненного выбора в остальной части вашего администратора приложения + обновление на form.js непосредственно, как это было бы сделать обновление Flask-Admin чрезвычайно громоздко)

в целом, я недоволен обоими решениями и этой витриной, что всякий раз, когда вы хотите выйти из треков фреймворка / инструмента, вы можете оказаться в сложных тупиках. Это может быть интересный запрос / проект функции для тех, кто готов внести свой вклад реальные решение вверх по течению к колбе-Admin.