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

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

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

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

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

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

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

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

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

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

Вы можете увидеть это, играя с сутью: нажмите на вкладку Recipe Arg. В первой строке («Серьезный отчет»), если вы попытаетесь отредактировать значение «Методарг», щелкнув по нему, будут доступны все четыре имени аргумента, а не только два.

# 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)

person droptable    schedule 11.11.2015    source источник
comment
Для гибкого интерфейса базы данных эта функция должна быть обязательно. Хороший ответ поможет многим людям. Конечно, сами данные можно структурировать по-разному, но суть не в этом. Добавление награды. :)   -  person Hans Schindler    schedule 14.11.2015


Ответы (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 и используется в качестве основы для Flask-Admin Select2Field) .

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

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

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

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

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

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

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

Этот фрагмент взят из form.js [1]

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

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

person bperson    schedule 23.11.2015
comment
Будет любопытно увидеть # 1, если вы покажете рабочий код, + 1 за решения. - person Hans Schindler; 24.11.2015
comment
Большое спасибо за ваш ответ. Страдаю от сильной простуды прямо сейчас, но с нетерпением жду возможности опробовать ваши идеи (не уверен, что полностью понимаю первое). Вы думаете, что что-то вроде вашей первой идеи будет самым надежным? Кстати, если вам нужно угадать, это та функция, которая на каком-то этапе должна попасть в flask-admin? - person droptable; 24.11.2015
comment
Hans Schindler@: Я постараюсь сделать что-нибудь для 1-. Мое самое большое препятствие сейчас — заставить Flask-Admin добавить атрибут data к тегам option. droptable@: надеюсь, получение кода для 1 сделает его более понятным;) Что касается самого надежного, то оба являются хаками, а 1 также имеет некоторые недостатки: вам нужно заранее получить список всех methodArgs, чтобы JS мог иметь достаточно большое изображение, чтобы сделать фильтрацию для вас. Наконец, я не являюсь сопровождающим Flask-Admin, я предполагаю, что это очень сложная функция, чтобы сделать ее достаточно правильной в общем, и ее все еще можно считать крайним случаем. - person bperson; 24.11.2015
comment
Я попробовал первое решение, но оно оказалось еще более громоздким, так как вам в основном приходится захватывать WTForms больше, чем Flask-Admin. Вы можете избавиться от обезьяньего патча, используя что-то вроде: flask -admin.readthedocs.org/en/latest/advanced/ (что также позволит вам не использовать этот data-mid на каждом select на вашей странице). stackoverflow.com/questions/23023788/ обсуждает эту проблему с добавлением пользовательских атрибутов к параметрам в файле WTForm SelectField. - person bperson; 25.11.2015
comment
Спасибо за потрясающую работу (принято), благодаря которой вы попали в flask-admin зал славы :) Это показывает, что простого пути нет. Я надеюсь, что команда flask-admin когда-нибудь добавит эту столь необходимую функцию. Пока забуду про flask-admin и сделаю свой интерфейс с нуля. - person droptable; 26.11.2015