Here's one way of achieving this. I've put the complete self-contained example on Github, flask-admin-modal.
Update 28 May 2018. The Github project has been enhanced by another user to handle form validation nicely.
In this example the SQLite database model is a Project with name (string) and cost (Integer) attributes and we will update the cost value for selected rows in the Flask-Admin list view. Note that the database is populated with random data when the Flask application is started.
Here's the model:
class Project(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(255), nullable=False, unique=True)
cost = db.Column(db.Integer(), nullable=False)
def __str__(self):
return unicode(self).encode('utf-8')
def __unicode__(self):
return "Name: {name}; Cost : {cost}".format(name=self.name, cost=self.cost)
Define a form with an integer cost field that accepts the new cost. This form also has a hidden field to keep track of the selected row ids.
class ChangeForm(Form):
ids = HiddenField()
cost = IntegerField(validators=[InputRequired()])
Override the list template for the Project view model. We do this so we can inject a Bootstrap modal form, with an id changeModal
, within the {% block body %}
, making sure we call {{ super() }}
first.
We also add a jQuery document ready function that will show the modal form if a template variable (change_modal
) evaluates to true. The same variable is used in the modal-body to display the change_form
. We use the Flask-Admin lib macros render_form to render the form in a Bootstrap style.
Note the value of the action parameter in render_form
- it is a route that we define in our Project view where we can process the form's data. Also note the the "Close" button has been replaced with a link, but still styled as a button. The link is the original url (including page and filter details) where the action was initiated from.
{% extends 'admin/model/list.html' %}
{% block body %}
{{ super() }}
<div class="modal fade" tabindex="-1" role="dialog" id="changeModal">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<a href="{{ url }}" class="close" aria-label="Close"><span aria-hidden="true">×</span></a>
<h4 class="modal-title">Change Project Costs</h4>
</div>
<div class="modal-body">
{% if change_modal %}
{{ lib.render_form(change_form, action=url_for('project.update_view', url=url)) }}
{% endif %}
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
{% endblock body %}
{% block tail %}
{{ super() }}
<script>
{% if change_modal %}
$(document).ready(function(){
$("#changeModal").modal('show');
});
{% endif %}
</script>
{% endblock tail %}
The Project view class needs to modify the behaviour of the batch action method and define a couple of routes that accepts POST requests.
@action('change_cost', 'Change Cost', 'Are you sure you want to change Cost for selected projects?')
def action_change_cost(self, ids):
url = get_redirect_target() or self.get_url('.index_view')
return redirect(url, code=307)
Instead of processing the ids directly the batch action gets the url that posted the action, this url will include any page number and filter details. It then does a redirect back to the list view with a 307. This ensures the selected rows ids are carried along in the body as well as the fact that it was a POST request.
Define a POST route to process this redirect, get the ids and url from the request body, instance a ChangeForm
, set the hidden ids
form field to an encoded list of the ids. Add the url
, change_form
and change_model
variables to the template args and then render the list view again - this time the modal popup form will be shown in the view.
@expose('/', methods=['POST'])
def index(self):
if request.method == 'POST':
url = get_redirect_target() or self.get_url('.index_view')
ids = request.form.getlist('rowid')
joined_ids = ','.join(ids)
encoded_ids = base64.b64encode(joined_ids)
change_form = ChangeForm()
change_form.ids.data = encoded_ids
self._template_args['url'] = url
self._template_args['change_form'] = change_form
self._template_args['change_modal'] = True
return self.index_view()
Define a POST route to process the modal form's data. This is standard form/database processing and when finished redirect back to the original url that initiated the action.
@expose('/update/', methods=['POST'])
def update_view(self):
if request.method == 'POST':
url = get_redirect_target() or self.get_url('.index_view')
change_form = ChangeForm(request.form)
if change_form.validate():
decoded_ids = base64.b64decode(change_form.ids.data)
ids = decoded_ids.split(',')
cost = change_form.cost.data
_update_mappings = [{'id': rowid, 'cost': cost} for rowid in ids]
db.session.bulk_update_mappings(Project, _update_mappings)
db.session.commit()
return redirect(url)
else:
# Form didn't validate
# todo need to display the error message in the pop-up
print change_form.errors
return redirect(url, code=307)
flash()
message instead ofprint change_form.errors
, and I removedcode=307
afterwards. (Nevertheless it is still looks like a magic for me.) – Bufflehead