Flask-admin batch action with argument via pop-up modal window - python

Is there any way to initiate a pop-up window from a Flask function?
I have a flask-admin + flask-sqlalchemy app. A table in the DB contains a field foo with some values. I have a UserAdmin view and I'm trying to create a batch action with some external argument.
I.e I want to:
select several elements from my DB and
substitute the old foo values for each of the element with the one new user-defined value and
the way I want to receive this new value is a modal window.
So the model is:
class User(db.Model):
# some code
foo = Column(Integer)
def change_foo(self, new_foo):
self.foo = int(new_foo)
db.session.commit()
return True
class UserAdmin(sqla.ModelView):
# some code
#action('change_foo', 'Change foo', 'Are you sure you want to change foo for selected users?')
def action_change_foo(self, ids):
query = tools.get_query_for_ids(self.get_query(), self.model, ids)
count = 0
# TODO: pop-up modal window here to get new_foo_value
# new_foo_value = ???
for user in query.all():
if user.change_foo(new_foo_value):
count += 1
flash(ngettext('Foo was successfully changed.',
'%(count)s foos were successfully changed.',
count, count=count))
except Exception as e:
flash(ngettext('Failed to change foo for selected users. %(error)s', error=str(e)), 'error')
I admit that the whole approach is not optimal, so I'd be glad to be advised with the better one.
There are some related questions: «Batch Editing in Flask-Admin» (yet unanswered) and «Flask-Admin batch action with form» (with some workaround using WTF forms).

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">
<span aria-hidden="true">×</span>
<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)

Related

Creating Custom Flask WTForms Widgets

I have a custom Flask WTForm where I want to have a portion of that form that includes a list of button type inputs that are created based on the number of entries in a table but have been having difficulties having them show up the way I want and passing the form validation. My goal for the look of this field is to have it show up as an Inline Button Group with a Checkbox type input. Below is an example of my route method.
#bp.route('/new_channel', methods=['GET', 'POST'])
def new_channel():
# Pre-populate the NewChannelForm
newChannelForm = NewChannelForm()
newChannelForm.required_test_equipment.choices = [(equip.id, equip.name) for equip in TestEquipmentType.query.order_by('name')]
test_equipment_types = TestEquipmentType.query.all()
return render_template('new_channel.html', title='Add New Channel', form=newChannelForm,
test_equipment_types=test_equipment_types)
I have tried using a FieldList with a FormField containing a custom form with a BooleanField and managed to get the styling right but the form validation didn't work. From looking into it further, BooleanField isn't compatible with a FieldList.
My next step is to use Flask WTForm example of a MultiSelectField with a custom widget for the Field and a custom widget for the option. The default is shown below:
class MultiCheckboxField(SelectMultipleField):
"""
A multiple-select, except displays a list of checkboxes.
Iterating the field will produce subfields, allowing custom rendering of
the enclosed checkbox fields.
"""
widget = widgets.ListWidget(prefix_label=False)
option_widget = widgets.CheckboxInput()
My goal is to modify this to make a custom widget called InLineButtonGroupWidget which will use the styling for a list of in-line buttons like my picture included before. Additionally, I am looking to create a custom option_widget called CheckboxButtonInput to get the styling of each individual button where I can pass info to the field. This is what I have as the goal for the both:
InLineButtonGroupWidget:
<div class="btn-group-toggle" role="group" data-toggle="buttons"></div>
CheckboxButtonInput:
<label class="btn btn-outline-info" for="check-1">Calibrator
<input type="checkbox" id="check-1">
</label>
The documentation for how to create custom widgets is a bit over my head and doesn't explain it the best so I'm looking for some
Edit:
Used Andrew Clark's suggestions and here is my final implementation:
routes.py
#bp.route('/new_channel', methods=['GET', 'POST'])
def new_channel():
class NewChannelForm(FlaskForm):
pass
test_equipment_types = TestEquipmentType.query.all()
for test_equipment_type in test_equipment_types:
# Create field(s) for each query result
setattr(NewChannelForm, f'checkbox_{test_equipment_type.name}', BooleanField(label=test_equipment_type.name, id=f'checkbox-{test_equipment_type.id}'))
newChannelForm = NewChannelForm()
if newChannelForm.validate_on_submit():
print('Form has been validated')
for test_equipment_type in test_equipment_types:
if newChannelForm.data[f'checkbox_{test_equipment_type.name}']:
channel.add_test_equipment_type(test_equipment_type)
return redirect(url_for('main.index'))
print(newChannelForm.errors.items())
return render_template('new_channel.html', title='Add New Channel', form=newChannelForm, units_dict=ENG_UNITS,
test_equipment_types=test_equipment_types)
new_channel.html
<!-- Test Equipment Selection -->
<div class="row">
<legend>Test Equipment Selection:</legend>
<div class="col-md-12">
<div class="btn-group-toggle mb-3" role="group" data-toggle="buttons">
{% for test_equipment_type in test_equipment_types %}
<label class="btn btn-outline-info" for="checkbox-{{ test_equipment_type.id }}">
{{ test_equipment_type.name }}
{{ form['checkbox_{}'.format(test_equipment_type.name)] }}
</label>
{% endfor %}
</div>
</div>
</div>
I usually tackle form building doing something like this:
def buildNewChannelForm():
class NewChannelForm(FlaskForm):
# put any non dynamic fields here
pass
test_equipment_types = TestEquipmentType.query.all()
for test_equipment_object in test_equipment_types:
# create field(s) for each query result
setattr(NewChannelForm, f'field_name_{test_equipment_object.id}', SelectField(label='label name', choices=[(equip.id, equip.name) for equip in TestEquipmentType.query.order_by('name')]))
return NewChannelForm()
Edit 1:
I'm not sure if there are better ways to do it, but I usually do something like this to handle data submission
def buildNewChannelForm():
new_channel_form_variable_list = []
class NewChannelForm(FlaskForm):
# put any non dynamic fields here
pass
test_equipment_types = TestEquipmentType.query.all()
for test_equipment_object in test_equipment_types:
# create field(s) for each query result
setattr(NewChannelForm, f'field_name_{test_equipment_object.id}', SelectField(label='label name', choices=[(equip.id, equip.name) for equip in TestEquipmentType.query.order_by('name')]))
# append variable name
new_channel_form_variable_list.append(f'field_name_{test_equipment_object.id}')
return NewChannelForm(), new_channel_form_variable_list
Then you can render your form using your variable list, just include in your render_template statement
{% for variable_name in new_channel_form_variable_list %}
{{ form[variable_name] }}
{% endfor %}
Then on submission of form in route, it's just a dictionary. So you can do something like this
result_dictionary = form.data
# either loop through your variable list or handle each one individually
for variable_name in new_channel_form_variable_list:
print(f'variable name: {variable_name}, value: {result_dictionary[variable_name]}')

How do I pass the database id from flask html into a post form with python

UPDATE:
I think my question is too confusing. To sum it up without all of the junk below. How can I pass a parameter in an HTML link into the POST function of my python file? I need that HTML parameter to be part of my commit to the database. My searching so far has not been successful.
I have populated an html page event_details.html with all the table events entries that have a matching id for table events_id row. In this page, I then have a link button to add another html page add_events_details.html to add a new entry to the database.
This link takes the user to another page with a wtform and I would like to pass the form the same events_id database row that is being matched to save the user entering this data.
So, the setup is, I pass the event_id of the entry to the add_events_details.html. But I can only pass this event_id to the get and not the post function, and it is in the post function where I need the event_id data so I can add it to the database.
How can I pass that event_id into my views.py post function so I can then add it to the database? Thank you in advance.
event_details.html
<div class="jumbotron">
{% if current_user.is_authenticated %}
<h1>{{ current_user }}</h1>
<p> This is the event details page </p>
Add Schedule
</div>
....more code... to show entries
add_event_details.html
<div class="jumbotron">
{% if current_user.is_authenticated %}
<h1>{{ current_user }}</h1>
<p> This is the event details page </p>
Add Schedule
</div>
And the views.py is
class AddEventDetailsView(MethodView):
decorators = [login_required]
template_file = 'add_event_details.html'
form_class = EventForm
def get(self, event_id):
return render_template(self.template_file, form=self.form_class())
def post(self):
form = self.form_class()
print(form.errors)
if form.validate_on_submit():
# I added this print to find out its type
print(type(form.events_id.data))
# This form returns the class model, so I just have to ask it for the \
# specific column that I want it to return
# events_id = form.events_id.data.id
user_id = current_user.id # This assumes that the person logged in is the company
eventdetails = Eventdetails(
schedule_name=form.schedule_name.data,
event_id=event_id,
user_id=user_id,
naics=form.naics.data
)
print(f"The user id is {user_id}")
db.session.add(eventdetails)
db.session.commit()
flash("Thank you for registering your event.")
return redirect(url_for('events.event_details', event_id=event_id))
I think I figured this out. I can just pass the same parameter into my POST request and use it that way and it works fine.
So, instead of
def get(self, event_id):
I just can do
def post(self, event_id):

Clean Django form fields with same name

I have a django template in which I'm dynamically rendering multiple fields (using ajax)
Below is a Django form (which has been rendered in a template) whose fields have same names. I want to use the cleaned_data method to clean form data in views.py before storing them in the database.
index.html
<div class="form-container">
<!-- ASSUMING I HAVE ALREADY ADDED FIELDS DYNAMICALLY -->
<form id = "orderForm" action="newPickupOrder/" method="post" name="processForm">
<input type='text' name='this_field'>
<input type='text' name='this_field'>
<button type="submit">Submit</button>
</form>
</div>
<form id="addItemForm">
{% csrf_token %}
<!-- BUTTON TO ADD MORE FIELDS DYNAMICALLY -->
<button id = "addItemButton">Add item</button>
</form>
<script>
var addItemButton = document.querySelector('#addItemButton');
addItemButton.onclick = function(){
$.ajax({
type: 'POST',
url: 'addItem/',
data: addItemForm.serialize(),
success: function (response) {
$("#orderForm").append(response);
console.log('Success');
},
error: function (response) {
console.log('Error = '+response);
}
});
};
</script>
forms.py
class ItemForm(forms.Form):
this_field = forms.CharField()
urls.py
urlpatterns = [
url(r'^newPickupOrder/$', views.pickup_order_view, name='new_pickup_order'),
]
views.py
def add_item(request):
if request.method == 'POST':
itemForm = ItemForm()
return HttpResponse(itemForm.as_p())
def pickup_order_view(request):
if request.method == 'POST':
form = ItemForm(request.POST)
same_name_fields = request.POST.getlist('this_field')
# WANT TO CLEAN DATA IN same_name_fields
if form.is_valid():
print(form.cleaned_data)
# ONLY PRINTS THE LAST FIELD's DATA
return HttpResponseRedirect('/viewPickupRequests')
The problem I'm facing is that if I use form.cleaned_data['this_field'], only the last field's data is fetched i.e. in this example, the field with value anotherTestValue is fetched and cleaned. If I fetch the data using request.POST.getlist('this_field'), all the fields' data is fetched and stored as a list, but, I don't know how to clean it using cleaned_data method. Is there a way to apply the cleaned_data method to the list of field data?
I'm sorry, I can't test if this works so this is not really an answer - but the comment system is not suitable for larger code chunks so I'm posting here.
Django forms lack a field type that renders to multiple text inputs with the same name. The proper thing to do would be to write a new form field class and a new widget. Since you are not rendering the form in the template (you are using it only for validation) I will omit the widget part.
class AcceptAnythingMultipleChoiceField(forms.MultipleChoiceField):
def validate(self, value):
if self.required and not value:
raise ValidationError(
self.error_messages['required'],
code='required'
)
Then use this field class instead of forms.CharField() (you may need to pass an empty choices parameter).
[update]
So essentially what you're saying is that I need to create new form field class and then render it to the template each time the user wants to add a new field? What if user has to add 15 fields, I'll need to create 15 classes then! I think this method won't be suitable in scenarios where number of fields required to be generated is large. I feel there should be some elegant way to do this which i'm not aware of – The OP
No, it is not what I'm saying. You probably want to subclass something like MultipleHiddenInput and set AcceptAnythingMultipleChoiceField.widget to it. You will have to create a new template based on the template for MultipleHiddenInput and replace input type="hidden" for type="text" (the original template is django/forms/widgets/multiple_hidden.html).
class AcceptAnythingWidget(MultipleHiddenInput):
template_name = 'django/forms/widgets/multiple_visible.html'
class AcceptAnythingMultipleChoiceField(forms.MultipleChoiceField):
widget = AcceptAnythingWidget
def validate(self, value):
if self.required and not value:
raise ValidationError(
self.error_messages['required'],
code='required'
)
This should render as many <input name='this_field'> as needed for instantiated forms at the frontend if you use:
{{ form.this_field }}
in the template, but will not add/remove them dynamically.
In order to do that you must plug in the JavaScript required to add/remove inputs dynamically in the widget but I will left this as an exercise for you. Look at Form Assets (the Media class) in the docs in order to figure out how to do that.
I think that what you are looking for is formsets. https://docs.djangoproject.com/en/2.0/topics/forms/formsets/
from django.forms import formset_factory
ItemFormSet = formset_factory(ItemForm, extra=2)
You can the essentialy use ItemFormSet in the way you would use a normal form except that this objects is iterable.
You will also have to change your jquery if you want to dynamically add items. There are many examples online on how to do this. In short what you do is
clone one of the forms in the formset
clear all the values from the copied form
update the input's (prefixes of) id's
Using Formsets doesn't solve the problem of fetching and validating
fields with same name. The issue still remains
It does however generate the end result you wanted (see below). My question would be why you need to have inputs with the same name? If there is some jquery stuff that uses these names I dont see any reason why you wouldn't be able to use name like... or assign a class to the inputs instead.
def pickup_order_view(request):
if request.method == 'GET':
ItemFormSet = formset_factory(ItemForm, extra=5)
item_formset = ItemFormSet()
template = "some_template.html"
template_context = {'item_formset': item_formset}
return render(request, template, template_context)
if request.method == 'POST':
ItemFormSet = formset_factory(ItemForm)
item_formset = ItemFormSet(request.POST)
same_name_fields=[]
if item_formset.is_valid():
for item_form in item_formset:
same_name_fields.append(item_form.cleaned_data['this_field'])
print(same_name_fields)
Template
<form id = "orderForm" action="newPickupOrder/" method="post" name="processForm">
{% csrf_token %}
{{ item_formset.management_form }}
{{ for item_form in item_formset }}
{{ item_form.as_p }}
{{ endfor }}
<input type='submit' value='submit'>
</form>
Go to newPickupOrder/ , fill in the 5 fields, hit submit, and watch it print your list.

How can I edit an object without saved it in Django

There is a problem with my Django project, when I add an object it saves immediatly after that I will be redirected by object id to server_edit where I can fill fields. If I fill no fields and push "back" (go to previous page) browser button object will be saved without any data even if Save button was not pushed on template.
Is there any way do not save object where no fields was filled?
How can I edit an object without saved it?
I have a model "Server" that contains a few CharField
class Server(models.Model):
name = models.CharField(max_length=256)
I add and save an object:
def server_add(request):
server = Server()
server.save()
return HttpResponseRedirect(reverse('server:server_edit', args=(server.id,)))
after this I redirect to edit page:
def server_edit(request, server_id):
server = get_object_or_404(Server, pk=server_id)
return render(request, 'server/server_edit.html'{'server': server})
Fields will be edited on html template:
<form action="{% url 'server:server_edit_post' server.id %}" method="post">
{% csrf_token %}
<tr>
<td>{% trans "Name:" %}</td>
<td><input type="text" name="name" maxlength="256" value="{{server.name}}" required></td>
</tr>
<button type="submit" class="btn btn-success">{% trans "Save" %}</button>
</form>
This view gets data from the template and allows to edit them:
def server_edit_post(request, server_id):
server= get_object_or_404(Server, pk=server_id)
name = request.POST['name']
server.name = name
server.save()
return HttpResponseRedirect(reverse('server:server_index', args=()))
You should avoid saving object and then filling it with data in different view.
Try using generic edit views such as CreateView/EditView or FormView with Django forms (https://docs.djangoproject.com/en/1.9/ref/class-based-views/generic-editing/).
Example:
class ServerCreateView(CreateView):
form_class = ServerCreateForm
template_name = 'servers/add.html'
With this, all validation is done automatically.
Http calls should be stateless. The connection can be dropped any time, which leaves the DB in the same inconsistent state which is what you try to avoid here. Instead of using the form which only contains name, you could just redirect the user to a new page with the form for the rest of the data with the previously entered name as a get parameter, and pre-fill that form with the name on that page

Using different 'Forms' for different methods

I hope i can express myself right, here it goes.
I have a handler for a html page that has the objective of editing a "chapter". In this page the chapters are listed and you can use a button to add more chapters. So the first time you open the page, the chapters are listed and a button to add more. If you click 'add', it should present you the same page but with a form for the chapter information.
My problem is passing the information of what chapters are we editing when we reload the page, because a can't pass the 'tut_key' - the reference to the chapters.
editTut.html:
{% for chap in chaps %}
Title: {{ chap.title}}<br>
{% endfor %}
{% if not editMode or editMode == 0 %}
<form ????????>
<input id="tutBtnNext" type="submit" value="Add">
</form>
{% endif %}
{% if editMode == 1 %}
<form method="post">
<!-- form stuff -->
</form>
{% endif %}
the class:
class EditTut(FuHandler):
def get(self):
tutID = self.request.get('tut_key')
tut = db.Key.from_path('Tutorial', tutID)
chaps = db.GqlQuery("SELECT * FROM Chapter " +
"WHERE tutorial = :1", tut)
self.render('editTut.html', chaps=chaps)
def post(self):
tutID = self.request.get("tut_key")
tutorial = db.Key.from_path('Tutorial', tutID)
title = self.request.get("chapTitle")
content = self.request.get("content")
note = self.request.get("note")
chap = Chapter(tutorial=tutorial, title=title,
content=content, note=note)
chap.put()
self.redirect('/editTut?tut_key=%s' % tutID)
#should i use something like this?
#I tried but i can't find a way to call this function on the html
def addChap(self):
tutID = self.request.get("tut_key")
self.redirect('/editTut?tut_key=%s' % tutID)
Setting a cookie would likely be a good option here. When they select a chapter you could send a cookie that specifies that option. Then, each time the page is loaded you just check for that cookie. If the cookie exists and is valid, you should know what chapter they are editing.
You can set cookies in Google App engine like this:
self.response.set_cookie('name', 'value', expires=expire_time, path='/', domain='example.com')
value could be something that references the chapter you're working with. If you leave expires blank, it should expire when they close the browser (which you may want).
You can get the cookie with:
self.request.cookies.get('name','')
Then you could just assign that cookie to a variable, and check it for the chapter info.
I hope I understand you correctly, I had to load forms based on criteria and did it like so:
if not editMode:
params = {}
form = 'add_mode.html'
else:
params = {"data", data}
form = 'edit_mode.html'
return self.render_template(form, **params)

Categories