Django – CRUD application with Python

Making a CRUD application with Python is actually one of the basic thing a Python Developer should be able to do. Although some may argue it is extremely basic – it is not.

Requirements

The basic requirements for this one, were the following:

  • Display “There are no recipes”, if there are no recipes written:
Index.html looks like this, when there are no recipes present.
  • Have a nice form to create a recipe:

    Creating a recipe is powered by the Forms in Django.
  • Delete the recipe with a blurred option:

    The form is not clicable, see the DisabledForm() class.
  • Display the recipes nicely, one below another:

    The index.html displays the recipes, if there are any.
  • Have a database (this is not Excel and VBA where you can save things in a spreadsheet 🙂 )
  • Show “details” nicely, with ingredients splitted by comma and shown to a list:

    The details.html page. The ingredients are splitted and listed.

Code

Ok, the code is actually available in GitHub (scroll a bit more), but here are some of the highlights:

Forms

The forms are part of the “Django” magic – they take the class which they model and render it into HTML page

from django import forms
from app.forms.common import DisabledForm
from app.models import Recipe

class RecipeForm(forms.ModelForm):
    class Meta:
        model = Recipe
        labels = {
            'time': 'Time (Minutes)',
            'image_url': 'Image URL',
        }
        fields = '__all__'

class DeleteRecipeForm(RecipeForm, DisabledForm):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        DisabledForm.__init__(self)

The DisabledForm() class is the one that makes the form look “unclickable”. See the “Delete Recipe” picture above to get an idea:

class DisabledForm():
    def __init__(self):
        for (_, field) in self.fields.items():
            field.widget.attrs['disabled'] = True
            field.widget.attrs['readonly'] = True

Views

The views are the part of the code, that take care about which html file should be presented with what content, upon a given request. In the project, these are into 2 files. Index.py is actually rather straight-forward, presenting either a simple html file with “There are no recipes” text on it or listing the recipes above each other. Which version you will see, depends on the value of the recipes_present key of the context:

from django.shortcuts import render
from app.models import Recipe

def index(request):
    recipes_present = Recipe.objects.exists()
    recipes = Recipe.objects.all()
    context = {
        'recipes_present': recipes_present,
        'recipes': recipes,
    }
    return render(request, 'index.html', context)

For the other recipes.py views the story is not that trivial. There we see a difference between GET and POST requests, for each of the CRUD actions. A pk variable is taken, in oder to locate exactly the correct recipe from the database with recipe = Recipe.objects.get(pk=pk):

from django.shortcuts import render, redirect

from app.forms.recipes import RecipeForm, DeleteRecipeForm
from app.models import Recipe


def recipe_create(request):
    if request.method == 'GET':
        context = {
            'form': RecipeForm(),
        }
        return render(request, 'create.html', context)
    else:
        form = RecipeForm(request.POST)
        if form.is_valid():
            form.save(commit=True)
            return redirect('index')
        context = {
            'form': form,
        }
        return render(request, 'create.html', context)


def recipe_edit(request, pk):
    recipe = Recipe.objects.get(pk=pk)

    if request.method == 'GET':
        context = {
            'recipe': recipe,
            'form': RecipeForm(instance=recipe)
        }
        return render(request, 'edit.html', context)
    else:
        form = RecipeForm(request.POST, instance=recipe)
        if form.is_valid():
            form.save()
            return redirect('index')

        context = {
            'recipe': recipe,
            'form': form,
        }
        return render(request, 'edit.html', context)


def recipe_details(request, pk):
    recipe = Recipe.objects.get(pk=pk)
    ingredients = recipe.ingredients.split(', ')
    context = {
        'recipe': recipe,
        'ingredients': ingredients,
    }
    return render(request, 'details.html', context)


def recipe_delete(request, pk):
    recipe = Recipe.objects.get(pk=pk)

    if request.method == 'GET':
        context = {
            'recipe': recipe,
            'form': DeleteRecipeForm(instance=recipe)
        }

        return render(request, 'delete.html', context)
    else:
        recipe.delete()
        return redirect('index')

Model

Our model is only one, as the app is quite small. Anyway, this one is used for the forms creation:

from django.db import models

class Recipe(models.Model):
    title = models.CharField(max_length=30)
    image_url = models.URLField()
    description = models.TextField()
    ingredients = models.CharField(max_length=250)
    time = models.IntegerField()

    def __str__(self):
        return f'{self.title} - {self.description}.'

Urls

There are two type of urls – /app/views/urls.py  and /recipes/urls.py. The old Roman saying – “Divide and rule” is actually quite an important one here, putting the recipe app logic into the recipe app and leaving almost nothing to the main app:

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('app.urls'))
]
urlpatterns = (
    path('', index, name='index'),
    path('create/', recipe_create, name='recipe create'),
    path('edit/<int:pk>', recipe_edit, name='recipe edit'),
    path('delete/<int:pk>', recipe_delete, name='recipe delete'),
    path('details/<int:pk>', recipe_details, name='recipe details'),
)

Templates

Partial template

As you have probably noticed that the upper part of the app is repeating everywhere, it is a good idea to take this part of the app into a partial template. It looks like this:

<html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <link rel="stylesheet" href="/static/css/style.css">
        <link rel="icon" type="image/png" href="/static/images/chef.png"/>
        <title>Recipes</title>
    </head>

    <body>
    <nav>
        <ul>
            <img src="/static/images/chef.png" alt="chef">
            <li class="title"><a href="{% url 'index' %}">Recipes</a></li>
            <li><a href="{% url 'recipe create' %}">Add Recipe</a></li>
        </ul>
    </nav>
    {% block site_content %}
    {% endblock %}
    </body>
</html>

Index.html

I have decided to put the Index, as you may easily see the If-Else-EndIf logic in this one, deciding whether to render the “No recipe” part of the page or the pictures with the text.

{% extends 'partials/site.html' %}
{% block site_content %}
<div class="container">
    <h1>My Recipes</h1>
    {% if recipes_present %}
        <div class="items-container">
            {% for recipe in recipes %}
                <div class="item">
                    <h2>{{ recipe.title }}</h2>
                    <img class="detail-img" src={{ recipe.image_url }} alt="recipe-image">
                    <div class="recipe-info">
                        {{ recipe.description }}
                    </div>
                    <div class="buttons-container">
                        <a class="button detail" href="{% url 'recipe details' recipe.id %}">Details</a>
                        <a class="button edit" href={% url 'recipe edit' recipe.id %}>Edit</a>
                        <a class="button delete" href={% url 'recipe delete' recipe.id %}>Delete</a>
                    </div>
                </div>
            {% endfor %}
        </div>

    {% else %}
        <h2>There are no recipes.</h2>
    {% endif %}

</div>
{% endblock %}

Delete.html

The delete is actually chosen, because it provides just a small taste of how little code is needed, if one uses the partial site and the forms correctly. 15 lines are enough:

{% extends 'partials/site.html' %}
{% block site_content %}
<div class="container">
    <h1>Delete Recipe</h1>
    <div class="items-container">
        <div class="item">
            <form method="POST">
                {{ form }}
                {% csrf_token %}
                <input type="submit" class="button delete" value="Delete">
            </form>
        </div>
    </div>
</div>
{% endblock %}

How to run it & GitHub:

I guess you probably will not be able to replicate the project with the code above. Don’t worry, there is a zip archive in git, that you can actually extract and run: https://github.com/Vitosh/Python_personal/tree/master/PythonProjects/recipes_su

Still, just having the GitHub will not be enough. There is a database behind it and if you run it without setting the database,  you would get: conn = _connect(dsn, connection_factory=connection_factory, **kwasync) django.db.utils.OperationalError: FATAL: database “recipes” does not exist

  • First make sure that you have a database named “recipes”;
  • Then make sure that you run migrate  from manage.py;
  • The settings.py is another story;