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:

- 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;