Adding parameter to render call when extending flask admin template - python

I am trying to add content to a Flask-admin list view.
I want to add my own content on top of the list view.
What I have done so far is to extend the default list view and added my own content like so :
{% extends 'admin/model/list.html' %}
{% block body %}
<h3>Submit report</h3>
{% if report_form.errors %}
<ul class="errors">
{% for field_name, field_errors in report_form.errors|dictsort if field_errors %}
{% for error in field_errors %}
<li>{{ form[field_name].label }}: {{ error }}</li>
{% endfor %}
{% endfor %}
</ul>
{% endif %}
<form action="{{ url_for('report.index') }}" method="post" enctype="multipart/form-data">
{{ report_form.file }}
{{ report_form.csrf_token }}
<button type="submit">Send</button>
</form>
{{ super() }}
{% endblock %}
And my custom model view for this template is :
class ReportAdmin(ModelView):
#expose('/', methods=('GET', 'POST'))
def index(self):
report_form = ReportFileForm()
if form.validate_on_submit():
file = form.file.data
#Check for report duplicate
if Report.query.filter(Report.filename == file.filename).all():
flash('Could not add report because a report with filename {} already exists.'.format(file.filename), 'error')
else:
try:
report = parser_factory(file)
flash('Report was submitted succesfully')
return redirect(url_for('report.index_view'))
except ValueError as e:
form.file.errors.append(e)
return self.render('report/index.html', report_form=report_form)
Now my problem is that the list view expects a certain number of parameters to be set (to handle displaying the list).
Those parameters are defined inside base.py
return self.render(
self.list_template,
data=data,
form=form,
delete_form=delete_form,
# List
list_columns=self._list_columns,
sortable_columns=self._sortable_columns,
editable_columns=self.column_editable_list,
# Pagination
count=count,
pager_url=pager_url,
num_pages=num_pages,
page=view_args.page,
# Sorting
sort_column=view_args.sort,
sort_desc=view_args.sort_desc,
sort_url=sort_url,
# Search
search_supported=self._search_supported,
clear_search_url=clear_search_url,
search=view_args.search,
# Filters
filters=self._filters,
filter_groups=self._filter_groups,
active_filters=view_args.filters,
# Actions
actions=actions,
actions_confirmation=actions_confirmation,
# Misc
enumerate=enumerate,
get_pk_value=self.get_pk_value,
get_value=self.get_list_value,
return_url=self._get_list_url(view_args),
)
So of course when trying to display the page, I get an error :
jinja2.exceptions.UndefinedError: 'num_pages' is undefined
My question is : how do I include my parameter into the render call of the parent view?
Thanks!

Try overriding the view's render method where you will be able to inject your variables into the kwargs argument.
Example (untested)
class ReportAdmin(ModelView):
list_template = 'report/index.html'
# Override to allow POSTS
#expose('/', methods=('GET', 'POST'))
def index_view(self):
return super(ReportAdmin, self).index_view(self)
def render(self, template, **kwargs):
# we are only interested in our custom list page
if template == 'report/index.html':
report_form = ReportFileForm()
if report_form.validate_on_submit():
file = report_form.file.data
#Check for report duplicate
if Report.query.filter(Report.filename == file.filename).all():
flash('Could not add report because a report with filename {} already exists.'.format(file.filename), 'error')
else:
try:
report = parser_factory(file)
flash('Report was submitted succesfully')
except ValueError as e:
report_form.file.errors.append(e)
kwargs['report_form'] = report_form
return super(ReportAdmin, self).render(template, **kwargs)

Related

Python Flask forms not capturing form input

I am running the latest Flask, Python 3.10. The app is installed on a Windows IIS Server. I have three forms on a single page. There is one submit button. Each form has a unique ID.
When I fill out one or all of the forms and hit the Submit button there is a view Class handler for the POST request. It is being called but the form data is missing.
One of the form elements looks like the following; the other two are similar but without the render_submit_field call:
<form id="add_session_form" action="{{ url_for('session_api') }}" method="POST" novalidate
role="form">
{{ session_form.hidden_tag() }}
{% for field in session_form %}
{% if not field.flags.hidden %}
{% if field.type == 'BooleanField' %}
{{ render_checkbox_field(field, tabindex=loop.index * 300) }}
{% elif field.type == 'SelectField' or field.type == 'SelectMultipleField' %}
{{ render_select_field(field, tabindex=loop.index * 300) }}
{% elif field.type == 'SubmitField' %}
{{ render_submit_field(field, tabindex=loop.index * 300) }}
{% else %}
{{ render_field(field, tabindex=loop.index * 300) }}
{% endif %}
{% endif %}
{% endfor %}
</form>
The POST method of the view Class looks like:
#staticmethod
def post():
try:
db_session = db()
school_form = SchoolForm(obj=current_user)
class_form = ClassForm(obj=current_user)
session_form = SessionForm(obj=current_user)
school_list = SchoolAPI.get_all_schools()
if school_form.validate_on_submit():
# Copy form fields to user_profile fields
school = School()
school_form.populate_obj(school)
db_session.add(school)
db_session.commit()
school_form = SchoolForm()
if class_form.validate_on_submit():
class_ = Class()
class_form.populate_obj(class_)
db_session.add(class_)
db_session.commit()
classes_form = ClassForm()
if session_form.validate_on_submit():
session = Session()
session_form.populate_obj(session)
db_session.add(session)
db_session.commit()
session_form = ClassForm()
return render_template(
'main/user_page.html',
current_user=current_user,
school_form=school_form,
class_form=class_form,
session_form=session_form,
school_list=school_list
)
except Exception as e:
return Response(repr(e), status=HTTPStatus.BAD_REQUEST)
The curious part is that this was working at the beginning of 2022. I was pulled onto another project and have recently been put back on this one. I updated all dependencies and am not seeing any errors.
I am not sure what to check at this point.
Ideas?

Flask with Jinja issue with pagination after the first page

I have created a dropdown menu to search for parts by project number from an SQL database. The first page loads with the correct parts but any pages subsequent give the error of:
TypeError: show_compound() missing 1 required positional argument: 'search_string'
From what I've seen online it seems I may need to use *args or pass the search_string to the template but I am unsure of how to use *args or where to insert the search_string value on the template.
#parts_database.route('/searchcompound', methods=['GET', 'POST'])
#login_required
def compounds_search():
form = ProjectSearch(request.form)
if form.validate_on_submit():
search_string = form.select.data.project_number
return show_compound(search_string)
return render_template('parts_database/search_compounds.html', form=form)
#parts_database.route('/showcompound', methods=['GET'])
#login_required
def show_compound(search_string):
page = request.args.get('page', 1, type=int)
pagination = PartsTable.query.filter_by(project_number=search_string).order_by(PartsTable.part_number).paginate(page, per_page=15, error_out=False)
compound = pagination.items
page_10 = pagination.next_num+9
if page_10 > pagination.pages:
pageincrement = pagination.pages
else:
pageincrement = page_10
page_decrement = page - 10
if page_decrement < 1:
page_decrement = 1
return render_template('parts_database/showpartstable.html', compound=compound, pagination=pagination, pageincrement=pageincrement, page_decrement=page_decrement)
template :
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% import "_macros.html" as macros %}
{% block title %}Amos{% endblock %}
{% block page_content %}
<div class="page-header">
<h1>Parts</h1>
{% include 'parts_database/_showpartstable.html' %}
{% if pagination %}
<div class="pagination">
{{ macros.pagination_widget(page_decrement, pageincrement, pagination, '.show_compound') }}
</div>
{% endif %}
</div>
{% endblock %}
If a view takes an argument, you must include that variable in the route.
In your case, you are missing search_string in your show_compound route definition. Try something like this:
#parts_database.route('/showcompound/<search_string>', methods=['GET'])
#login_required
def show_compound(search_string):
(...)
EDIT:
Also, I'd sugest to redirect instead of calling another view's function.
Replace this:
if form.validate_on_submit():
search_string = form.select.data.project_number
return show_compound(search_string)
with this:
You'll have to import redirect before that:
from flask import redirect # (Add this at the top)
(...)
if form.validate_on_submit():
search_string = form.select.data.project_number
return redirect(url_for('parts_database.show_compound', search_string=search_string))

django - What does this line achieve here?

I'm following a tutorial on effectivedjango.com, and this is the code they have:
views.py:
class CreateContactView(CreateView):
model = Contact
template_name = 'edit_contact.html'
fields = '__all__' #this is needed for error msg Using ModelFormMixin (base class of CreateContactView) without the 'fields' attribute is prohibited.
def get_success_url(self):
return reverse('contacts-list')
def get_context_data(self, **kwargs):
context = super(CreateContactView, self).get_context_data(**kwargs)
context['action'] = reverse('contacts-new')
return context
class UpdateContactView(UpdateView):
model = Contact
template_name = 'edit_contact.html'
fields = '__all__'
def get_success_url(self):
return reverse('contacts-list')
def get_context_data(self, **kwargs):
context = super(UpdateContactView, self).get_context_data(**kwargs)
context['action'] = reverse('contacts-edit', kwargs={'pk' : self.get_object().id})
return context
urls.py:
url(r'^$', contacts.views.ListContactView.as_view(),
name='contacts-list',),
url(r'^new$', contacts.views.CreateContactView.as_view(),
name='contacts-new',),
url(r'^edit/(?P<pk>\d+)/$', contacts.views.UpdateContactView.as_view(),
name='contacts-edit',),
contact_list.html:
{% block content %}
<h1>Contacts</h1>
<ul>
{% for contact in object_list %}
<li class="contact">
{{ contact }}
(edit)
</li>
{% endfor %}
</ul>
Add contact
{% endblock %}
edit_contact.html:
{% block content %}
{% if contact.id %}
<h1>Edit Contact</h1>
{% else %}
<h1>Add Contact</h1>
{% endif %}
<form action="{{ action }}" method="POST">
{% csrf_token %}
<ul>
{{ form.as_ul }}
</ul>
<input id="save_contact" type="submit" value="Save" />
</form>
Back to list
{% if contact.id %}
Delete
{% endif %}
{% endblock %}
Why does the line context['action'] = reverse('contacts-edit', kwargs={'pk' : self.get_object().id}) in views.py look like its calling itself?
What I mean is, this action is called when the submit button is pressed in the contact-edit template, correct? So it starts there, and it is reverse-calling contact-edit which is itself, right?
What am I not seeing here?
Thank you for all your help.
Yes, the line context['action'] = reverse('contacts-edit', kwargs={'pk' : self.get_object().id}) in views.py is calling itself. This line generates the proper url for contacts-edit view.
This is done so that POST requests come to the same view i.e. UpdateContactView which is an UpdateView. There, proper handling will be done i.e. form validation will occur with the sent data. If the form is valid, object will be updated. Otherwise, the form will be displayed again with errors.
Django docs on UpdateView:
A view that displays a form for editing an existing object,
redisplaying the form with validation errors (if there are any) and
saving changes to the object.

Adding custom button or a tag to django admin

I want to add some html element (button, "a" tag, etc ) to a django admin page. How can i do it? Please help.
Not sure where you want to add your stuff but this is a solution I found somewhere else on SO to change the HTML of a FileField (in my case i wanted to display the current image in an ImageField).
In other words you can make a widget that modifies the html of the field you want to customize:
# Widget that modifies the output of a FileField
class OutputWidget(AdminFileWidget):
# Overloaded django magic
def render(self, name, value, attrs=None):
output = []
# This is the place where we edit the output
if value and getattr(value, "url", None):
image_url = value.url
output.append(u' <img src="%s" alt="%s" />' % (image_url, image_url, image_url))
output.append(super(AdminFileWidget, self).render(name, value, attrs))
return mark_safe(u''.join(output))
# ModelAdmin class that is applied to the model
class MyModelSettings(admin.ModelAdmin):
# Overloaded django magic
def formfield_for_dbfield(self, db_field, **kwargs):
# Look for the field we want to edit and register the widget with it
if db_field.name == 'nameOfFieldIWantToEdit':
request = kwargs.pop("request", None)
kwargs['widget'] = OutputWidget
return db_field.formfield(**kwargs)
return super(MyModelSettings,self).formfield_for_dbfield(db_field, **kwargs)
# Register my overloaded settings with the model
admin.site.register(MyModel, MyModelSettings)
The code goes into admin.py where you register your models.
From the docs:
The Django admin site
Customizing the Django admin interface
I used Omokoli's solution from above but to make the field use my custom widget I did:
class MyModelAdminForm(forms.ModelForm):
class Meta:
model = get_model('myapp', 'mymodel')
widgets = {
'original_link': OutputWidget,
}
You can create a file under templates/admin called base_site.html
(create the "admin" folder in your app).
add this code:
{% extends "admin/base_site.html" %}
{% load i18n %}
{% block userlinks %}
{% if site_url %}
{% trans 'View site' %} /
{% endif %}
{% if user.is_active and user.is_staff %}
{% url 'django-admindocs-docroot' as docsroot %}
{% if docsroot %}
{% trans 'Documentation' %} /
{% endif %}
{% endif %}
{% if user.has_usable_password %}
{% trans 'Change password' %} /
{% endif %}
{% trans 'Log out' %}
<!-- YOUR CUSTOM CODE HERE -->
<div class="your_custom_class">
{% trans 'your link 1' %} /
{% trans 'your link 2' %}
</div>
{% endblock %}
You can overwrite a lot of the admin html files. Make sure you extends the html you are overwriting, not to loose basic functionality.
See customize-django-admin-python for full admin templates tree you can overwrite.
You can also look at the base_site.html in Django project in Github. You'll find all of the "base_site" blocks you can plant your custom code in.

Django Formsets - form.is_valid() is False preventing formset validation

I'm am utilizing a formset to enable users subscribe to multiple feeds. I require a) Users chose a subscription by selecting a boolean field, and are also required to tag the subscription and b) a user must subscribe to an specified number of subscriptions.
Currently the below code is capable of a) ensuring the users tags a subscription, however some of my forms is_valid() are False and thus preventing my validation of the full formset. [edit] Also, the relevant formset error message fails to display.
Below is the code:
from django import forms
from django.forms.formsets import BaseFormSet
from tagging.forms import TagField
from rss.feeder.models import Feed
class FeedForm(forms.Form):
subscribe = forms.BooleanField(required=False, initial=False)
tags = TagField(required=False, initial='')
def __init__(self, *args, **kwargs):
feed = kwargs.pop("feed")
super(FeedForm, self).__init__(*args, **kwargs)
self.title = feed.title
self.description = feed.description
def clean(self):
"""apply our custom validation rules"""
data = self.cleaned_data
feed = data.get("subscribe")
tags = data.get("tags")
tag_len = len(tags.split())
self._errors = {}
if feed == True and tag_len < 1:
raise forms.ValidationError("No tags specified for feed")
return data
class FeedFormSet(BaseFormSet):
def __init__(self, *args, **kwargs):
self.feeds = list(kwargs.pop("feeds"))
self.req_subs = 3 # TODO: convert to kwargs arguement
self.extra = len(self.feeds)
super(FeedFormSet, self).__init__(*args, **kwargs)
# WARNING! Using undocumented. see for details...
def _construct_form(self, i, **kwargs):
kwargs["feed"] = self.feeds[i]
return super(FeedFormSet, self)._construct_form(i, **kwargs)
def clean(self):
"""Checks that only a required number of Feed subscriptions are present"""
if any(self.errors):
# Do nothing, don't bother doing anything unless all the FeedForms are valid
return
total_subs = 0
for i in range(0, self.extra):
form = self.forms[i]
feed = form.cleaned_data
subs = feed.get("subscribe")
if subs == True:
total_subs += 1
if total_subs != self.req_subs:
raise forms.ValidationError("More subscriptions...") # TODO more informative
return form.cleaned_data
As requested, the view code:
from django.forms import formsets
from django.http import Http404
from django.http import HttpResponseRedirect
from django.shortcuts import render_to_response
from rss.feeder.forms import FeedForm
from rss.feeder.forms import FeedFormSet
from rss.feeder.models import Feed
FeedSet = formsets.formset_factory(FeedForm, FeedFormSet)
def feeds(request):
if request.method == "POST":
formset = create_feed_formset(request.POST)
if formset.is_valid():
# submit the results
return HttpResponseRedirect('/feeder/thanks/')
else:
formset = create_feed_formset()
return render_to_response('feeder/register_step_two.html', {'formset': formset})
def create_feed_formset(data=None):
"""Create and populate a feed formset"""
feeds = Feed.objects.order_by('id')
if not feeds:
# No feeds found, we should have created them
raise Http404('Invalid Step')
return FeedSet(data, feeds=feeds) # return the instance of the formset
Any help would be appreciated.
Ps. For full disclosure, this code is based on http://google.com/search?q=cache:rVtlfQ3QAjwJ:https://www.pointy-stick.com/blog/2009/01/23/advanced-formset-usage-django/+django+formset
[Solved] See solution below.
Solved. Below is a quick run through of the solution.
Reporting the error required manipulating and formating a special error message. In the source code for formsets I found the errors that apply to a whole form are known as non_form_errors and produced a custom error based on this. [note: I couldn't find any authoritive documentation on this, so someone might know a better way]. The code is below:
def append_non_form_error(self, message):
errors = super(FeedFormSet, self).non_form_errors()
errors.append(message)
raise forms.ValidationError(errors)
The formsets clean method also needed a few tweaks. Basically it checks the if the forms is bound (empty ones aren't, hence is_valid is false in the question) and if so accesses checks there subscribe value.
def clean(self):
"""Checks that only a required number of Feed subscriptions are present"""
count = 0
for form in self.forms:
if form.is_bound:
if form['subscribe'].data:
count += 1
if count > 0 and count != self.required:
self.append_non_form_error("not enough subs")
Some might wonder why I choose to access the value using the form['field_name'].data format. This allows us to retrieve the raw value and always get a count on subscriptions, allowing me to return all relevant messages for the entire formset, i.e. specific problems with individual forms and higher level problems (like number of subscriptions), meaning that the user won't have to resubmit the form over and over to work through the list of errors.
Finally, I was missing one crucial aspect of my template, the {{ formset.non_form_errors }} tag. Below is the updated template:
{% extends "base.html" %}
{% load i18n %}
{% block content %}
<form action="." method="post">
{{ formset.management_form }}
{{ formset.non_form_errors }}
<ol>
{% for form in formset.forms %}
<li><p>{{ form.title }}</p>
<p>{{ form.description }}</p>
{{ form.as_p }}
</li>
{% endfor %}
</ol>
<input type="submit">
</form>
{% endblock %}
I made attempt to circumvent my problem...it is not a good solution, it's very much so a hack. It allows people to proceed if they subscribe to the required number of feeds (in the case below more than 1), however if less than the required number of feeds, it fails to show the error message raised.
def clean(self):
count = 0
for i in range(0, self.extra):
form = self.forms[i]
try:
if form.cleaned_data:
count += 1
except AttributeError:
pass
if count > 1:
raise forms.ValidationError('not enough subscriptions')
return form.cleaned_data
I do use {{ formset.management_form }} in my template so as far as I know the error should display. Below my template in case I'm misguided.
{% extends "base.html" %}
{% load i18n %}
{% block content %}
<form action="." method="post">
{{ formset.management_form }}
<ol>
{% for form in formset.forms %}
{{ form.as_p }}
</li>
{% endfor %}
</ol>
<input type="submit">
</form>
{% endblock %}

Categories