Django model + Frontend CRUD + pagination + API REST and filters by Example

makiolo

Ricardo

Posted on January 30, 2021

Django model + Frontend CRUD + pagination + API REST and filters by Example

Table Of Contents

Create model

We type in base dir:

python manage.py startapp stocks
Enter fullscreen mode Exit fullscreen mode

Register app in settings.py:

INSTALLED_APPS = [
    ...
    'stocks',
]
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Apply migrations:

python manage.py makemigrations
python manage.py migrate
Enter fullscreen mode Exit fullscreen mode

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']

Enter fullscreen mode Exit fullscreen mode

Then you should be see something like this in your django admin:

Alt Text

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']
Enter fullscreen mode Exit fullscreen mode

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')

Enter fullscreen mode Exit fullscreen mode

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')

Enter fullscreen mode Exit fullscreen mode

In settings.urls we add:

...
from stocks.urls import stocks_patterns
...

urlpatterns = [
    ...
    path('stocks/', include(stocks_patterns)),
    ...
]
Enter fullscreen mode Exit fullscreen mode

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">&hellip;</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">&hellip;</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 %}

Enter fullscreen mode Exit fullscreen mode

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 %}

Enter fullscreen mode Exit fullscreen mode

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 %}


Enter fullscreen mode Exit fullscreen mode

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 %}


Enter fullscreen mode Exit fullscreen mode

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 %}
Enter fullscreen mode Exit fullscreen mode

I use 'core/base.html' from django-bulma. Here the result:

Alt Text

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
Enter fullscreen mode Exit fullscreen mode

And register it in settings:

INSTALLED_APPS = [
    ...
    'rest_framework',
    'django_filters',
]
Enter fullscreen mode Exit fullscreen mode

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__'
Enter fullscreen mode Exit fullscreen mode

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__'

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)),
    ...
]
Enter fullscreen mode Exit fullscreen mode

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.

Greats. Ricardo.
Follow me for more content.

Ricardo's DEV Community Profile

💖 💪 🙅 🚩
makiolo
Ricardo

Posted on January 30, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related