formulario de administración del matraz: restringir el valor del campo 2 según el valor del campo 1

Una característica que me ha costado implementar en flask-admin es cuando el usuario edita un formulario, para restringir el valor del Campo 2 una vez que se ha establecido el Campo 1.

Permítanme dar un ejemplo simplificado en palabras (el caso de uso real es más complicado). Luego mostraré una idea completa que implementa ese ejemplo, menos la característica “restringir”.

Digamos que tenemos una base de datos que rastrea algunas “recetas” de software para generar informes en varios formatos. La tabla de recipe de nuestra base de datos de muestra tiene dos recetas: “Informe serio”, “Arte ASCII”.

Para implementar cada receta, elegimos uno entre varios métodos. La tabla de method de nuestra base de datos tiene dos métodos: “tabulate_results”, “pretty_print”.

Cada método tiene parámetros. La tabla methodarg tiene dos nombres de parámetros para “tabulate_results” (“rows”, “display_total”) y dos parámetros para “pretty_print” (“embellishment_character”, “lines_to_jump”).

Ahora, para cada una de las recetas (“Informe serio”, “Arte ASCII”) debemos proporcionar el valor de los argumentos de sus respectivos métodos (“tabulate_results”, “pretty_print”).

Para cada registro, la tabla recipearg nos permite seleccionar una receta (es decir, el Campo 1, por ejemplo, “Informe serio”) y un nombre de argumento (es el Campo 2). El problema es que se muestran todos los nombres de los argumentos posibles, mientras que deben estar restringidos en función del valor del Campo 1.

¿Qué mecanismo de filtrado / restricción podemos implementar de manera tal que una vez que seleccionamos “Informe serio”, sepamos que usaremos el método “tabulate_results”, de modo que solo estén disponibles los argumentos de “filas” y “display_total”?

Estoy pensando en alguna magia de AJAX que comprueba el Campo 1 y establece una consulta para los valores del Campo 2, pero no tengo idea de cómo proceder.

Puedes ver esto jugando con la esencia: haz clic en la pestaña Recipe Arg . En la primera fila (“Informe serio”), si intenta editar el valor de “Methodarg” haciendo clic en él, los cuatro nombres de los argumentos están disponibles, en lugar de solo dos.

 # 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 'Click me to get to Admin!' 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) 

Veo dos formas de abordar este problema:

1- Cuando Flask-Admin genere el formulario, agregue atributos de data con la mid de cada methodArg en cada etiqueta de option en la selección de methodArg . Luego haga que un código JS filtre las tags de option según la receta seleccionada.

EDITAR

Aquí hay un bash tentativo de poner un atributo de data-mid en cada option :

 def monkeypatched_call(self, field, **kwargs): kwargs.setdefault('id', field.id) if self.multiple: kwargs['multiple'] = True html = ['') return HTMLString(''.join(html)) Select.__call__ = monkeypatched_call 

El bloqueador está en el hecho de que esas llamadas de procesamiento se activan desde las plantillas jinja, por lo que está bastante atascado al actualizar un widget ( Select es el más bajo en WTForms y se usa como base para Select2Field Flask-Admin) .

Después de obtener esos data-mid de cada una de sus opciones, puede continuar con solo vincular un change en la selección de su receta y mostrar la option de methodarg que tenga un valor medio de data-mid coincidentes. Teniendo en cuenta que Flask-Admin utiliza select2 , es posible que tenga que hacer algunos ajustes de JS (la solución más sencilla sería limpiar el widget y volver a crearlo para cada evento de change activado)

En general, me parece que esta es menos robusta que la segunda solución. Mantuve el Monkeypatch para dejar en claro que esto no debería usarse en la producción de imho. (La segunda solución es un poco menos intrusiva).

2- Utilice la finalización de ajax admitida en Flask-Admin para obtener las opciones que desee según la receta seleccionada:

Primero, cree un AjaxModelLoader personalizado que será responsable de ejecutar la consulta de selección correcta en la base de datos:

 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 

Luego, actualice form.js Flask-Admin para que el navegador le envíe la información de la receta en lugar del nombre de methodArg que debe completarse methodArg . (o puede enviar tanto en la query como hacer un análisis de arg en su AjaxLoader, ya que Flask-Admin no realiza ningún análisis en la query , esperando que sea una cadena que supongo [0] . De esa manera, mantendría la autocompletación)

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

Este fragmento de form.js está tomado de form.js de Flask-Admin [1]

Obviamente, esto necesita algunos ajustes y parametrización (porque hacer una solución tan intrincada le impedirá usar otra selección poblada por ajax en el rest de la aplicación admin + la actualización en form.js directamente, lo que haría que la actualización de Flask-Admin extremadamente incómoda )

En general, estoy insatisfecho con ambas soluciones y con este escaparate que cuando quiera salir de las pistas de un marco / herramienta, puede terminar en callejones sin salida complejos. Esta podría ser una solicitud / proyecto de función interesante para alguien que esté dispuesto a contribuir con una solución real en sentido ascendente a Flask-Admin.