I have a Django Model Formset that is rendered with a crispy-forms in a custom table inline formset template. The formset is a list of alerts with entities, logic, and comments. Entity and logic are populated in each form, and the user writes a comment prior to submitting the form.
The formset currently has 140 forms (i.e. 140 records in the Alert model), and it will need to handle more than that number. It takes 4-5 minutes for alerts.html to render. I can use Django-based pagination to limit the queryset and reduce rendering time, but this precludes me from using something like jQuery datatables to paginate and quickly add JS functionality.
I used django-debug-toolbar to review. The queries are definitely funky and duplicated per form (i.e. the query grabbing entity information is duplicated for each form and also includes duplicate where statements). However, it only takes 3-4 seconds for the queries to run, so the problem must be elsewhere. I used Chrom dev tools to record the performance, and the Waiting (TTFB) time is the cause of the performance issue. I understand that this involves server-side processing, which leads me to believe it is a Django issue.
Why is the render taking so long? Is there anything in my code below that could be optimized? I know caching is an option, but I believe this mostly affects query performance which doesn't appear to be the driving issue. Is there any other piece of django-debug-toolbar that might shed more light on timing, outside of just the queries?
EDIT: Based on the comments, I determined the include tags in the table_inline_formset.html template are causing the issue because they are rendering hundreds of the same templates. I created another question here to address this problem: Crispy-Forms Include Tag Causing Many Duplicate Templates.
models.py:
class Logic(models.Model):
logic = models.CharField(max_length=50)
def __str__(self):
return self.logic
class Entity(models.Model):
entity = models.CharField(primary_key=True, max_length=12)
entityType = models.CharField(max_length=10)
entityDescription = models.CharField(max_length=200)
def __str__(self):
return '%s - %s - %s' % (self.entity, self.entityType, self.entityDescription)
class Alert(models.Model):
entity = models.ForeignKey(Entity, on_delete=models.CASCADE, db_column='entity')
logic = models.ForeignKey(Logic, on_delete=models.CASCADE, db_column='logic')
comment = models.CharField(max_length=500)
def __str__(self):
return '%s' % self.entity
forms.py:
AlertFormSet = modelformset_factory(Alert, extra=1, exclude=(), form=AlertForm)
class AlertFormsetHelper(FormHelper):
def __init__(self, *args, **kwargs):
super(AlertFormsetHelper, self).__init__(*args, **kwargs)
self.form_method = 'post'
self.template = 'alerts/table_inline_formset.html'
self.add_input(Submit("submit", "Submit"))
self.layout = Layout(
Field('entity', css_class="input"),
Field('logic', css_class="input"),
Field('comment', css_class="input")
)
views.py:
def alerts(request):
newAlerts = Alert.objects.filter(disposition='')
formset = AlertFormSet(request.POST or None, queryset=newAlerts)
helper = AlertFormsetHelper()
context = {'formset':formset, 'helper':helper}
if request.method == 'POST':
for form in formset:
if form.is_valid():
if form.has_changed():
if form.is_valid():
form.save()
return HttpResponseRedirect('/alerts')
return render(request, 'alerts/alerts.html', context)
table_inline_formset.html:
{% load crispy_forms_tags %}
{% load crispy_forms_utils %}
{% load crispy_forms_field %}
{% specialspaceless %}
{% if formset_tag %}
<form {{ flat_attrs|safe }} method="{{ form_method }}" {% if formset.is_multipart %} enctype="multipart/form-data"{% endif %}>
{% endif %}
{% if formset_method|lower == 'post' and not disable_csrf %}
{% csrf_token %}
{% endif %}
<div>
{{ formset.management_form|crispy }}
</div>
<div class='table-responsive'>
<table{% if form_id %} id="{{ form_id }}_table"{% endif%} class="table table-hover table-sm" id='dispositionTable'>
<thead>
{% if formset.readonly and not formset.queryset.exists %}
{% else %}
<tr>
{% for field in formset.forms.0 %}
{% if field.label and not field|is_checkbox and not field.is_hidden %}
<th for="{{ field.auto_id }}" class="form-control-label {% if field.field.required %}requiredField{% endif %}">
{{ field.label|safe }}{% if field.field.required %}<span class="asteriskField">*</span>{% endif %}
</th>
{% endif %}
{% endfor %}
</tr>
{% endif %}
</thead>
<tbody>
{% for form in formset %}
{% if form_show_errors and not form.is_extra %}
{% include "bootstrap4/errors.html" %}
{% endif %}
<tr>
{% for field in form %}
{% include 'bootstrap4/field.html' with tag="td" form_show_labels=False %}
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% include "bootstrap4/inputs.html" %}
{% if formset_tag %}</form>{% endif %}
{% endspecialspaceless %}
alerts.html:
{% crispy formset helper %}
Related
I have a Django formset that is displayed as a table with one form per table. I would like to add a checkbox in the first column of the table so that the user to check it if they would like to delete the row (form).
I have the javascript to manage the deletion of the formset row (form) and modify the management form on the front end, but I am having an issue when I add the DELETE field to the form. I used the solution reference in modify DELETE widget so that I could add the class "delete" to all of my delete fields for use in CSS and JS on the front end. When the DELETE field is added it is always the last field in the form. I would like it to be first.
models.py
class ModelOne(models.Model):
attr_one = models.CharField(max_length=16)
attr_two = models.CharField(max_length=16)
forms.py
class BaseFormOneFormset(BaseModelFormSet):
def add_fields(self, form, index) -> None:
super().add_fields(form, index)
form.fields[DELETION_FIELD_NAME].widget = forms.CheckboxInput(attrs={'class':"delete"})
form.fields["id"].widget=forms.HiddenInput(attrs={'class':'pk'})
class FormOne(forms.ModelForm):
class Meta:
model = ModelOne
attr_one = forms.CharField(max_length=16,
required=True,
label="attr_one",
widget=forms.TextInput(attrs={'size':5,'class':'required '}))
attr_two = forms.CharField(max_length=16,
required=True,
label="attr_two",
widget=forms.TextInput(attrs={'size':5,'class':'required '}))
views.py
def view_one(request):
formset_factory_one = modelformset_factory( ModelOne,
FormOne,
formset=BaseFormOneFormset,
extra=0,
can_delete=True)
formset_one = formset_factory_one(query_set=FormOne.objects.all(),
prefix="formone")
return render(request, "app_one/template_one.html",{"formset_one":formset_one})
template_one.html
<table id="tbl-id">
<thead id="tbl-head-id">
<tr>
{% for form in formset_one|slice:":1" %}
{% for field in form.visible_fields %}
<th>{{field.label|safe}}</th>
{% endfor %}
{% endfor %}
</tr>
</thead>
<tbody id="tbl-body-id">
{% for form in formset_one %}
<tr id="row{{forloop.counter0}}-id" class="formset-row">
{% for field in form.visible_fields %}
<td>
{{field}}
</td>
{% endfor %}
{% for field in form.hidden_fields %}
<td hidden >{{field}}</td>
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
Resulting Table
image of resulting html table
Solutions tried
I have already tried to set the Form.field_order both in the FormOne class declaration
class FormOne(forms.ModelForm):
field_order = [ DELETION_FIELD_NAME, "attr_one", "attr_two"]
and in the BaseFormset.add_fields method
def add_fields(self, form, index) -> None:
super().add_fields(form, index)
form.fields[DELETION_FIELD_NAME].widget = forms.CheckboxInput(attrs={'class':"delete"})
form.fields["id"].widget=forms.HiddenInput(attrs={'class':'pk'})
form.field_order = [ DELETION_FIELD_NAME, "attr_one", "attr_two"]
These both result in the DELETE field still last in the order.
Although there are ways to change the order of the DELETE field by overriding the formset's add_fields() function, it is pretty tedious and kinda complicated.
However, if your goal is to simply render DELETE first, or in a completely different part of the page, you are in luck. This is easily accomplished in the template.
The delete field can be rendered explicitly in the template using {{ form.DELETE }} which is equivalent to the regular output of {{ field }} that you get when iterating through form.fields.
Simple example to render DELETE field first:
{# this renders the DELETE field #}
{{ form.DELETE }}
{# now render the other fields #}
{% for field in form.visible_fields %}
{# if check to prevent DELETE rendering twice #}
{% if field.name != 'DELETE' %}
{{ field }}
{% endif %}
{% endfor %}
Applied to the code from your question:
<table id="tbl-id">
<thead id="tbl-head-id">
<tr>
{% for form in formset_one|slice:":1" %}
{% for field in form.visible_fields %}
<th>Delete</th>
{% if field.name !='DELETE' %}
<th>{{field.label|safe}}</th>
{% endif %}
{% endfor %}
{% endfor %}
</tr>
</thead>
<tbody id="tbl-body-id">
{% for form in formset_one %}
<tr id="row{{forloop.counter0}}-id" class="formset-row">
<td>
{{ form.DELETE }}
</td>
{% for field in form.visible_fields %}
{% if field.name != 'DELETE' %}
<td>
{{field}}
</td>
{% endif %}
{% endfor %}
{% for field in form.hidden_fields %}
<td hidden >{{field}}</td>
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
I am new to Django and don't understand what really is causing this:
I have a Model Company which has an OneToOneField, creator.
# models.py
class Company(models.Model):
class Meta:
verbose_name = 'Company'
verbose_name_plural = 'Companies'
creator = models.OneToOneField(User, related_name="company", on_delete=models.CASCADE, unique=False, null=True)
name = models.CharField(max_length=50)
I have a TemplateView class to handle get and post requests for creating a Company model:
# views.py
class create_company(TemplateView):
def get(self, request):
form = CompanyCreateForm()
title = "Some form"
return render(request, "form.html", {"form": form, "title": title})
def post(self, request):
form = CompanyCreateForm(request.POST)
if form.is_valid():
comp = form.save(commit=False)
comp.creator = request.user
comp.save()
return redirect('index')
The form is showing correctly also storing when I submit, the problem I am facing is with base.html where I show {% user.company %}; the form template extends it like:
{% extends "account/base.html" %}
{% load crispy_forms_tags %}
{% block content %}
<div class="container">
<form method="post" action="">
{% csrf_token %}
{{form|crispy}}
<button class="btn btn-success" type="submit">Save</button>
</form>
<br>
</div>
<br>
{% endblock %}
and in base.html I access
{% if user.is_authenticated %}
{% user.company %}
{% endif %}
But user.company is not showing even it is set; it shows only when I redirect to index but not when I render the form.
Can someone help me understand what causes this?
{% if request.user.is_authenticated %}
{% request.user.company %}
{% endif %}
you are not sending any context to the base.html, thus only user wont work.
This was the error when I simulated your code.
Error during template rendering
In template /home/user/django/drf_tutorial/snippets/templates/base.html, error at line 2
Invalid block tag on line 2: 'user.company', expected 'elif', 'else' or 'endif'. Did you forget to register or load this tag?
1 {% if user.is_authenticated %}
2 {% user.company %}
3 {% endif %}
4 {% block content %}{% endblock %}
It gives hint that the code to show company should be variable {{ }} instead of tag {% %}. So the base.html template should be as below.
{% if user.is_authenticated %}
{{ user.company }}
{% endif %}
{% block content %}{% endblock %}
Here are my codes(it works fine):
#views.py
class IndexView(generic.ListView):
template_name = 'index.html'
context_object_name = 'home_list'
queryset = Song.objects.all()
paginate_by = 1
def get_context_data(self, **kwargs):
context = super(IndexView, self).get_context_data(**kwargs)
context['all_artists']=Artist.objects.all()
context['all_songs']=Song.objects.all()
context['all_albums']=Album.objects.all()
return context
base.html(which is extended by index.html):
#base.html
{% block content %}{% endblock %}
{% block pagination %}
{% if is_paginated %}
<div class="pagination">
<span class="page-links">
{% if page_obj.has_previous %}
Previous
{% endif %}
<span class="page-current">
Page {{page_obj.number}} of {{page_obj.paginator.num_pages }}
</span>
{% if page_obj.has_next %}
Next
{% endif %}
</span>
</div>
{% endif %}
{% endblock %}
And my index.html:
{% extends 'base_generic.html' %}
{% block title %}<title>Listen to songs </title>{% endblock %}
{% block content %}
<h3>Best Songs</h3>
{% for song in all_songs %}
<ol>
<li>{{song.song_title}} <img src="{{song.song_logo}}" heigt=112, width=114/> <br></li>
</ol>
{% endfor %}
<h3>Best Albums</h3>
{% for album in all_albums %}
<ul>
<li title="{{album.album_title}}">
<img id="img_{{album.id}}" src="{{album.album_logo}}" heigt=112, width=114 />
<p>{{album.album_title}}</p>
</li>
</ul>
{% endfor %}
{% endblock %}
So when I compiled this, I got this window :
Image here
But in all pages, it stays the same.What I want is to display 1 song per page.Help guys !!!! :] :] :]
You never use your paginated objects, instead you've made a separate context variable called all_songs.
Simply just use the right context data
{% for song in all_songs %}
should be
{% for song in home_list %}
You may want to apply pagination for your other querysets too although it can get confusing paginating more than one list
Have a look here: https://www.youtube.com/watch?v=q-Pw7Le30qQ
The video explains pagination.
Alternative: https://docs.djangoproject.com/en/1.10/topics/pagination/
If you only want to display one song there is always the option to use a DetailView, which will only show one item.
Here is a stackoverflow question that describes the process for class based views: How do I use pagination with Django class based generic ListViews?
In your example: you don't have to set the queryset. Remove queryset = ### and add model = #YOURMODELNAME#.
If you want to overwrite the queryset you should do it in def get_queryset() which is a function of ListView. Like this:
class SongView(ListView):
model = Song
template_name = 'template_name'
def get_queryset():
queryset = super(SongView, self).get_queryset(**kwargs)
queryset = #aditional filters, ordering, whatever#
return queryset
i have an app called reviews
reviews/forms.py
from django.forms import ModelForm, Textarea
from reviews.models import Review
class ReviewForm(ModelForm):
class Meta:
model = Review
fields = ['rating', 'comment']
widgets = {
'comment': Textarea(attrs={'cols': 40, 'rows': 15}),
}
reviews/views.py
from django.shortcuts import get_object_or_404, render
from django.http import HttpResponseRedirect
from django.core.urlresolvers import reverse
from .models import Review, Wine
from .forms import ReviewForm
import datetime
from django.contrib.auth.decorators import login_required
#login_required
def add_review(request, wine_id):
wine = get_object_or_404(Wine, pk=wine_id)
form = ReviewForm(request.POST)
if form.is_valid():
rating = form.cleaned_data['rating']
comment = form.cleaned_data['comment']
user_name = form.cleaned_data['user_name']
user_name = request.user.username
review = Review()
review.wine = wine
review.user_name = user_name
review.rating = rating
review.comment = comment
review.pub_date = datetime.datetime.now()
review.save()
# Always return an HttpResponseRedirect after successfully dealing
# with POST data. This prevents data from being posted twice if a
# user hits the Back button.
return HttpResponseRedirect(reverse('reviews:wine_detail', args=(wine.id,)))
return render(request, 'reviews/wine_detail.html', {'wine': wine, 'form': form})
reviews/templates/reviews/wine_detail.html
{% extends 'base.html' %}
{% load bootstrap3 %}
{% block title %}
<h2>{{ wine.name }}</h2>
<h5>{{ wine.review_set.count }} reviews ({{ wine.average_rating | floatformat }} average rating)</h5>
{% endblock %}
{% block content %}
<h3>Recent reviews</h3>
{% if wine.review_set.all %}
<div class="row">
{% for review in wine.review_set.all %}
<div class="col-xs-6 col-lg-4">
<em>{{ review.comment }}</em>
<h6>Rated {{ review.rating }} of 5 by {{ review.user_name }}</h6>
<h5><a href="{% url 'reviews:review_detail' review.id %}">
Read more
</a></h5>
</div>
{% endfor %}
</div>
{% else %}
<p>No reviews for this wine yet</p>
{% endif %}
<h3>Add your review</h3>
{% if error_message %}<p><strong>{{ error_message }}</strong></p>{% endif %}
<form action="{% url 'reviews:add_review' wine.id %}" method="post" class="form">
{% csrf_token %}
{% bootstrap_form form layout='inline' %}
{% buttons %}
<button type="submit" class="btn btn-primary">
{% bootstrap_icon "star" %} Add
</button>
{% endbuttons %}
</form>
{% endblock %}
base.html
{% load bootstrap3 %}
{% bootstrap_css %}
{% bootstrap_javascript %}
{% block bootstrap3_content %}
<div class="container">
<nav class="navbar navbar-default">
<div class="navbar-header">
<a class="navbar-brand" href="{% url 'reviews:review_list' %}">Winerama</a>
</div>
<div id="navbar" class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li>Wine list</li>
<li>Home</li>
</ul>
<ul class="nav navbar-nav navbar-right">
{% if user.is_authenticated %}
<li>Hello {{ user.username }}</li>
<li>Logout</li>
{% else %}
<li>Login</li>
<li>Register</li>
{% endif %}
</ul>
</div>
</nav>
<h1>{% block title %}(no title){% endblock %}</h1>
{% bootstrap_messages %}
{% block content %}(no content){% endblock %}
</div>
{% endblock %}
I am getting the error at the line {% bootstrap_form form layout='inline' %} in the html file
Any idea how to fix this?
There's a few problems with your code as it stands, so I'll try to clean it up with some comments as I would write it to add a review to a wine.
#login_required
def add_review(request, wine_id):
wine = get_object_or_404(Wine, pk=wine_id)
if request.POST:
form = ReviewForm(request.POST)
else:
form = ReviewForm()
if form.is_valid():
### NO NEED FOR - already set as part of valid modelform ::: rating = form.cleaned_data['rating']
### AS WELL AS ::: comment = form.cleaned_data['comment']
### THIS IS NOT A FIELD IN YOUR FORM :::user_name = form.cleaned_data['user_name']
user_name = request.user.username
review = form.save(commit=False) # commit = False means that this instantiate but not save a Review model object
review.wine = wine
review.user_name = user_name # Why use this instead of a ForeignKey to user?
review.pub_date = datetime.datetime.now() # works as long as pub_date is a DateTimeField
review.save() # save to the DB now
# Always return an HttpResponseRedirect after successfully dealing
# with POST data. This prevents data from being posted twice if a
# user hits the Back button.
return HttpResponseRedirect(reverse('reviews:wine_detail', args=(wine.id,))) # THIS will redirect only upon form save
return render(request, 'reviews/wine_detail.html', {'wine': wine, 'form': form})
Now, the error your seeing is most likely related to you passing request.POST to a form even if request.POST is blank; the form will attempt to set initial values but with a querydict that has no values that actually relates to the form.
EDIT: In response to your comments, my next step would be to try and render each form field individually and see if I can trigger a failure.
Instead of {% bootstrap_form form layout='inline' %}, try-
{% for field in form %}
{% bootstrap_field field %}
{% endfor %}
If this is an error with the django-bootstrap library trying to render the textarea widget and the inline style together (as I would suspect at this point), you can also eliminate the widget parameter and see if there's a fix. If there is, I'd suggest overriding your modelform's init method for assign a widget post a call super on init.
In Class Base View
This error may occur when you use form_class in the wrong generic view.
⮕ Open your views.py then check to see if you have set the wrong generic view in your class.
Example
class ProfileUpdateView(T̶e̶m̶p̶l̶a̶t̶e̶V̶i̶e̶w̶ UpdateView):
model = User
form_class = forms.ProfileForm
success_url = reverse_lazy("stories:story_list")
template_name = 'profile.html'
def get_object(self, queryset=None):
return get_object_or_404(User, pk=self.request.user.id)
I've been using django built-in pagination (is_paginated) in few of my pages. They are all working fine. Except for the search page where the pagination should only appear based on the filtered queryset.
I've checked through few other thread but it ain't helping much.
How do I use pagination with Django class based generic ListViews?
Django template tag exception
Here's a mini version of what I have so far:-
1)views.py
class SearchBookView(ListView):
template_name = 'books/search_book.html'
paginate_by = '2'
context_object_name = 'book'
form_class = SearchBookForm
def get(self, request):
form = self.form_class(request.GET or None)
if form.is_valid():
filtered_books = self.get_queryset(form)
context = {
'form' : form,
'book' : filtered_books,
}
else:
context = {'form': form}
return render(request, self.template_name, context)
def get_queryset(self, form):
filtered_books = Book.objects.all()
if form.cleaned_data['title'] != "":
filtered_books = filtered_books.filter(
title__icontains=form.cleaned_data['title'])
return filtered_books
def get_context_data(self):
context = super(SearchBookView, self).get_context_data()
return context
2) search_book.html (template)
{% crispy form %}
{% if book %}
<p>Found {{ book|length }} book{{ book|pluralize }}.</p>
{% for book in book %}
<div class="card">
<div style="height:170px; border:solid #111111;" class="col-md-3">
Ima
</div>
<div class="whole-card col-md-9">
<div class="title">"{{ book.title }}"</div>
<div>{{ book.description }}</div>
Read More
</div>
</div>
{% endfor %}
{% else %}
<p>No book matched your searching criteria.</p>
{% endif %}
{% if is_paginated %}
<div class="pagination">
<span class="page-links">
{% if page_obj.has_previous %}
previous
{% endif %}
<span class="page-current">
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}.
</span>
{% if page_obj.has_next %}
next
{% endif %}
</span>
</div>
{% endif %}
forms.py
class SearchBookForm(forms.Form):
title = forms.CharField(max_length=20)
def __init__(self, *args, **kwargs):
self.helper = FormHelper()
self.helper.add_input(Submit('search', 'Search', css_class='btn'))
self.helper.form_method = 'GET'
self.helper.layout = Layout('title')
super(SearchBookForm, self).__init__(*args, **kwargs)
------------------UPDATE------------------
Though I understand Daniel Roseman's answer but as I am fairly new to django, I am not sure how to implement the whole thing, hitting plenty of "X not accessible, X is not attribute of Y" and etc. After much digging, I found some other useful posts on this same matter.
Django: Search form in Class Based ListView
Updating context data in FormView form_valid method?
Django CBV: Easy access to url parameters in get_context_data()?
Django class based view ListView with form
URL-parameters and logic in Django class-based views (TemplateView)
Another problem I encounter is I am unable to access the parameters in URL using self.kwargs as what suggested in most of the posts. In the final link I posted above, Ngenator mentioned that URL parameters has to be accessed using request.GET.get('parameter'). I used that and it's working fine for me.
By combining everything, here's the revised piece of coding I have. Just in case anyone is having the same problem as me.
1) views.py
class SearchBookView(ListView):
template_name = 'books/search_book.html'
paginate_by = '3'
context_object_name = 'book_found'
form_class = SearchBookForm
model = Book
def get_queryset(self):
object_list = self.model.objects.all()
title = self.request.GET.get('title', None)
if title is not None and title != "":
object_list = object_list.filter(title__icontains=title)
else:
object_list = []
return object_list
def get_context_data(self):
context = super(SearchBookView, self).get_context_data()
form = self.form_class(self.request.GET or None)
context.update({
'form': form,
})
return context
2) search_book.html (template)
{% extends "base.html" %}
{% load crispy_forms_tags %}
{% load staticfiles %}
{% load bootstrap_pagination %}
{% block title %}Search Page{% endblock %}
{% block content %}
<div class="container">
{% if form.errors %}
<p style="color: red;">
Please correct the error{{ form.errors|pluralize }} below.
</p>
{% endif %}
{% crispy form %}
{% if book_found %}
<p>Found {{ paginator.count }} book{{ book_found_no|pluralize }}.</p>
{% for book in book_found %}
<div class="wholecard">
<div style="height:170px; border:solid #111111;" class="col-md-3">
Image
</div>
<div class="card col-md-9">
<div class="card-title">"{{ book.title }}"</div>
<div>{{ book.description }}</div>
Read More
</div>
</div>
{% endfor %}
{% else %}
<p>No book matched your searching criteria.</p>
{% endif %}
{% bootstrap_paginate page_obj %}
</div>
{% endblock %}
And I ended up using jmcclell's bootstrap-pagination also for pagination. Saved me lots of time! Good stuff...
You've specifically overridden the get method so that it defines its own context, and never calls the default methods, so naturally none of the default context bars are available.
Don't do that; you should almost never be overriding the get and post methods. You should probably move all the form stuff directly into get_queryset.
It's working
views.py
class UserListView(ListView):
model = User
template_name = 'user_list.html'
context_object_name = 'users'
paginate_by = 10
def get_queryset(self):
return User.objects.all()
templates/user_list.html
{% if is_paginated %}
<nav aria-label="Page navigation conatiner">
<ul class="pagination justify-content-center">
{% if page_obj.has_previous %}
<li>« PREV </li>
{% else %}
<li class="disabled page-item"><a class="page-link">PREV !</a></li>
{% endif %}
{% for i in %}
{{ i }}
{% endfor %}
{% if page_obj.has_next %}
<li> NEXT »</li>
{% else %}
<li class="disabled page-item"><a class="page-link">NEXT !</a></li>
{% endif %}
</ul>
</nav>
</div>
{% endif %}