Django model + Frontend CRUD + pagination + API REST and filters by Example
Ricardo
Posted on January 30, 2021
Table Of Contents
Create model
We type in base dir:
python manage.py startapp stocks
Register app in settings.py:
INSTALLED_APPS = [
...
'stocks',
]
We write initial model in "models.py":
from django.db import models
class Stock(models.Model):
id = models.AutoField(primary_key=True)
name = models.CharField(max_length=128)
price = models.FloatField()
created = models.DateTimeField(auto_now_add=True)
update = models.DateTimeField(auto_now=True)
class Meta:
verbose_name = "stock"
verbose_name_plural = "stocks"
ordering = ['-id', ]
def __str__(self):
return self.name
Apply migrations:
python manage.py makemigrations
python manage.py migrate
Create your ModelAdmin:
from django.contrib import admin
from django.contrib.admin import register
from stocks.models import Stock
@register(Stock)
class StockAdmin(admin.ModelAdmin):
list_display = ['id', 'name', 'price', 'update']
Then you should be see something like this in your django admin:
Create frontend
Create a ModelForm for after render it. Create new file "forms.py".
from django import forms
from .models import Stock
class StockForm(forms.ModelForm):
class Meta:
model = Stock
fields = ['name', 'price']
Now, we should create a CRUD views in file "views.py" (note , i add staff authentication required in update, create and delete but not is list amd not in detail):
from django.urls import reverse_lazy
from django.views.generic import ListView, DetailView, CreateView, UpdateView, DeleteView
from .models import Stock
from .forms import StockForm
from django.utils.decorators import method_decorator
from django.contrib.admin.views.decorators import staff_member_required
class StockListView(ListView):
model = Stock
paginate_by = 2 # change it, is page size
def get_queryset(self):
query = self.request.GET.get('q')
if query:
object_list = self.model.objects.filter(name__icontains=query)
else:
object_list = self.model.objects.all()
return object_list
class StockDetailView(DetailView):
model = Stock
@method_decorator(staff_member_required, name='dispatch')
class StockCreate(CreateView):
model = Stock
form_class = StockForm
template_name_suffix = '_create_form'
success_url = reverse_lazy('stocks:list')
@method_decorator(staff_member_required, name='dispatch')
class StockUpdate(UpdateView):
model = Stock
form_class = StockForm
template_name_suffix = '_update_form'
def get_success_url(self):
return reverse_lazy('stocks:update', args=(self.object.id, )) + '?ok'
@method_decorator(staff_member_required, name='dispatch')
class StockDelete(DeleteView):
model = Stock
success_url = reverse_lazy('stocks:list')
We connect this views to url paths (create "urls.py"):
from django.urls import path
from .views import StockListView, StockDetailView, StockCreate, StockUpdate, StockDelete
stocks_patterns = ([
path('', StockListView.as_view(), name='list'),
path('<int:pk>/<slug:slug>/', StockDetailView.as_view(), name='detail'),
path('create/', StockCreate.as_view(), name='create'),
path('update/<int:pk>', StockUpdate.as_view(), name='update'),
path('delete/<int:pk>', StockDelete.as_view(), name='delete'),
], 'stocks')
In settings.urls we add:
...
from stocks.urls import stocks_patterns
...
urlpatterns = [
...
path('stocks/', include(stocks_patterns)),
...
]
This CRUD views need next 5 templates:
- stocks/templates/stocks/stock_list.html
- stocks/templates/stocks/stock_detail.html
- stocks/templates/stocks/stock_create_form.html (custom prefix)
- stocks/templates/stocks/stock_update_form.html (custom prefix)
- stocks/templates/stocks/stock_confirm_delete.html
stocks/templates/stocks/stock_list.html:
{% extends 'core/base.html' %}
{% load static %}
{% block title %}Stocks{% endblock %}
{% block content %}
<form method='GET'>
<div class="columns is-centered mb-1">
<div class="column is-11">
<p class="control has-icons-left">
<input class='input' type='text' name='q' value='{{ request.GET.q }}' placeholder="Search">
<span class="icon is-left">
<i class="fas fa-search" aria-hidden="true"></i>
</span>
</p>
</div>
<div class="column">
<input class="button is-primary" type='submit' value="Search">
</div>
</div>
</form>
{% if stock_list|length == 0 %}
No se han encontrado resultados.
<a href="{% url 'stocks:create' %}" class="card-footer-item">Create</a>
{% endif %}
{% for stock in stock_list %}
<div class="card">
<header class="card-header">
<p class="card-header-title">
{{ stock.name }}
</p>
<a href="#" class="card-header-icon" aria-label="more options">
<span class="icon">
<i class="fas fa-angle-down" aria-hidden="true"></i>
</span>
</a>
</header>
<div class="card-content">
<div class="content">
{{stock.name|striptags|safe|truncatechars:"200"}} {{stock.price}}
<br />
<time datetime="2016-1-1">{{stock.updated}}</time>
</div>
</div>
<footer class="card-footer">
{% if request.user.is_staff %}
<a href="{% url 'stocks:create' %}" class="card-footer-item">Create</a>
<a href="{% url 'stocks:delete' stock.id %}" class="card-footer-item">Delete</a>
<a href="{% url 'stocks:update' stock.id %}" class="card-footer-item">Edit</a>
{% endif %}
<a href="{% url 'stocks:detail' stock.id stock.name|slugify %}" class="card-footer-item">Read more ...</a>
</footer>
</div>
<br />
{% endfor %}
{% if stock_obj.paginator.num_pages > 1 %}
<nav class="pagination" role="navigation" aria-label="pagination">
{% if stock_obj.has_previous %}
<a class="pagination-previous" href="?page={{ stock_obj.previous_page_number }}&q={{ request.GET.q }}">Previous</a>
{% endif %}
{% if stock_obj.has_next %}
<a class="pagination-next" href="?page={{ stock_obj.next_page_number }}&q={{ request.GET.q }}">Next page</a>
{% endif %}
<ul class="pagination-list">
{% if stock_obj.number > 3 %}
<li>
<a class="pagination-link" aria-label="Goto page 1" href="?page=1&q={{ request.GET.q }}">1</a>
</li>
{% if stock_obj.number > 4 %}
<li>
<span class="pagination-ellipsis">…</span>
</li>
{% endif %}
{% endif %}
{% for i in stock_obj.paginator.page_range %}
<li>
{% with leftmax=stock_obj.number|add:"-3" %}
{% with rightmax=stock_obj.number|add:"+3" %}
{% if leftmax < i %}
{% if i < rightmax %}
{% if i == stock_obj.number %}
<a class="pagination-link is-current" aria-label="Goto page {{ i }}" href="?page={{ i }}&q={{ request.GET.q }}" aria-current="page">{{ i }}</a>
{% else %}
<a class="pagination-link" aria-label="Goto page {{ i }}" href="?page={{ i }}&q={{ request.GET.q }}">{{ i }}</a>
{% endif %}
{% endif %}
{% endif %}
{% endwith %}
{% endwith %}
</li>
{% endfor %}
{% with rightdistance=stock_obj.paginator.num_pages|add:"-2" %}
{% with rightdistanceplus=stock_obj.paginator.num_pages|add:"-3" %}
{% if stock_obj.number < rightdistance %}
{% if stock_obj.number < rightdistanceplus %}
<li>
<span class="pagination-ellipsis">…</span>
</li>
{% endif %}
<li>
<a class="pagination-link" aria-label="Goto page {{ stock_obj.paginator.num_pages }}" href="?page={{ stock_obj.paginator.num_pages }}&q={{ request.GET.q }}">{{ stock_obj.paginator.num_pages }}</a>
</li>
{% endif %}
{% endwith %}
{% endwith %}
</ul>
</nav>
{% endif %}
{% endblock %}
stocks/templates/stocks/stock_detail.html:
{% extends 'core/base.html' %}
{% load static %}
{% block title %}{{object.name}}{% endblock %}
{% block content %}
<main role="main">
<div class="container">
<div class="row mt-3">
<div class="col-md-9 mx-auto">
<h2 class="title">{{object.name}}</h2>
<div>
name: {{object.name|safe}}<br />
price: {{object.price}}<br />
{% if request.user.is_staff %}
<br />
<p><a href="{% url 'stocks:list' %}">Volver</a></p>
<p><a href="{% url 'stocks:update' object.id %}">Editar</a></p>
{% endif %}
</div>
</div>
</div>
</div>
</main>
{% endblock %}
stocks/templates/stocks/stock_create_form.html:
{% extends 'core/base.html' %}
{% load bulma_tags %}
{% load static %}
{% block title %}Create{% endblock title %}
{% block content %}
<form method="post">
{% csrf_token %}
{{ form | bulma }}
<div class="field">
<button type="submit" class="button is-primary">Create</button>
</div>
<input type="hidden" name="next" value="{{ next }}"/>
</form>
<p><a href="{% url 'stocks:list' %}">Volver</a></p>
{% endblock content %}
stocks/templates/stocks/stock_update_form.html:
{% extends 'core/base.html' %}
{% load bulma_tags %}
{% load static %}
{% block title %}Update{% endblock title %}
{% block content %}
{% if 'ok' in request.GET %}
<article class="message is-primary">
<div class="message-header">
<p>Actualizado</p>
<button class="delete" aria-label="delete"></button>
</div>
<div class="message-body">
Updated. <a href="{% url 'stocks:detail' stock.id stock.name|slugify %}">more details here.</a>
</div>
</article>
{% endif %}
<form method="post">
{% csrf_token %}
{{ form|bulma }}
<div class="field">
<button type="submit" class="button is-primary">Update</button>
</div>
<input type="hidden" name="next" value="{{ next }}"/>
</form>
<p><a href="{% url 'stocks:list' %}">Volver</a></p>
{% endblock content %}
stocks/templates/stocks/stock_confirm_delete.html:
{% extends 'core/base.html' %}
{% load static %}
{% block title %}{{object.name}}{% endblock %}
{% block content %}
<div class="box has-text-centered">
<form action="" method="post">{% csrf_token %}
<p class="mb-3">¿Estás seguro de que quieres borrar <b>"{{ object }}"</b>?</p>
<input class="button is-primary" type="submit" value="Sí, borrar la página" />
<input class="button is-danger" onclick="history.go(-1); return false;" value="Cancelar" />
</form>
</div>
{% endblock %}
I use 'core/base.html' from django-bulma. Here the result:
We have search, pagination, edit form, create form and delete. All this, with minimal code and very customizable.
Create API
For expose this model to API REST, we use django restframework and django-filter:
$ pip install djangorestframework
$ pip install django-filter
And register it in settings:
INSTALLED_APPS = [
...
'rest_framework',
'django_filters',
]
For convert model to json, or json to model we need create "serializers.py" file, and type:
from rest_framework import serializers
from .models import Stock
class StockSerializer(serializers.ModelSerializer):
class Meta:
model = Stock
fields = '__all__'
For create a API Rest that let "filter" and "order" for someone field of model. We create "filters.py" and type:
import django_filters
from .models import Stock
class StockFilter(django_filters.FilterSet):
id = django_filters.NumberFilter()
name = django_filters.CharFilter(lookup_expr='iexact')
price = django_filters.CharFilter(lookup_expr='eq')
order = django_filters.OrderingFilter(fields=('name', 'price', ))
class Meta:
model = Stock
fields = '__all__'
Finally, we need create a API View using your serializer and your filter. We create "viewsets.py" and type:
from rest_framework import viewsets
from .models import Stock
from .serializers import StockSerializer
from .filters import StockFilter
class StockViewSet(viewsets.ModelViewSet):
queryset = Stock.objects.all()
serializer_class = StockSerializer
filterset_class = StockFilter
Now, we are registering this viewset in settings.router:
from rest_framework import routers
from stocks.viewsets import StockViewSet
router = routers.DefaultRouter()
router.register('stocks', StockViewSet)
urlpatterns = [
...
path('api/v1/', include(router.urls)),
...
]
Maybe you need a good config for restframework.auth. Using JWT, Cookies, or whatever you want.
The API (is full CRUD + filters) is available in: http://localhost:8000/api/v1/stocks/
I recommend "drf-spectacular" for generate API documentation in swagger, or redoc format.
I hope this is useful for someone.
Posted on January 30, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.