Dom Vacchiano
Posted on June 16, 2022
In this tutorial we'll build a recipes app in Django where users can register and create, read, update, and delete recipes. I will also link videos 📽️ associated with each step below.
Final Product:
Step 1: Project Setup
By the end of this step we'll have a Django project running. We'll be using python 3.8. Anything above 3.3 should work.
Create a virtual environment for the project so it's packages are separated from others on your computer. In your terminal type:
Windows:
>> python -m venv env
>> .\env\scripts\activate
Mac:
>> python3 -m venv env
>> source env/scripts/activate
Next we need to install Django, in your terminal type:
>> pip install django==3.2.*
Create a Django project. Each Django project may consist of 1 or more apps, in your terminal type (the '.' creates in current dir):
>> django-admin startproject config .
Run the server and view localhost:8000. In your terminal type (type ctr+c to stop the server):
>> python manage.py runserver
🍾🎉 Woohoo our Django project is up and running!
Step 2: Django Apps, URLs, and Views
By the end of this step we'll have a web page with text!
1) Create your first Django app, in the terminal type:
>> python manage.py startapp recipes
2) Open config/settings.py and add 'recipes' under installed apps
# config/settings.py
INSTALLED_APPS = [
...
'django.contrib.staticfiles',
#local apps
'recipes',
]
3) Let's create a view to return something to the user. Open the recipes/views.py file, import HttpResponse and create a function home(request) that returns the HttpResponse with some text:
# recipes/views.py
from django.http import HttpResponse
def home(request):
return HttpResponse('<h1>Welcome to the Recipes home page</h1>')
4) Create a new file in the recipes folder 'urls.py' for the routing. Import views.py from the same dir and add a new path to the urlpatterns for the home route:
# recipes/urls.py
from django.urls import path
from . import views
urlpatterns = [
path('', views.home, name="recipes-home"),
]
5) Update the urls.py file in the config folder to include the urls from the recipes app. in config/urls.py add:
# config/urls.py
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('recipes.urls')),
]
Go ahead and run the server again, go to localhost:8000 and you should see 'Welcome to the Recipes home page' 🚀
Step 3: HTML Templates + UI
In this step we'll return an actual html page!
1) Create a templates dir to hold the files with this path: recipes/templates/recipes
2) In that templates/recipes folder create a file 'base.html' and let's add some html that all our pages will use:
# /templates/recipes/base.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
{% if title %}
<title>Django Recipes - {{title}}</title>
{% else %}
<title>Django Recipes</title>
{% endif %}
</head>
<body>
{% block content %} {% endblock %}
</body>
</html>
3) Create another html template 'home.html' and add some html for the homepage, let's pass in some dummy data:
# templates/recipes/home.html
{% extends "recipes/base.html" %}
<!-- -->
{% block content %}
<h1>Recipes:</h1>
{% for recipe in recipes %}
<h1>{{recipe.title}}</h1>
<p>{{recipe.author}} | {{recipe.date_posted}}</p>
<h5>{{recipe.content}}</h5>
<hr />
{% endfor %}
<!-- -->
{% endblock content %}
4) Now let's update the home view and pass the dummy data. In the recipes/views.py file add:
# recipes/views.py
recipes = [
{
'author': 'Dom V.',
'title': 'Meatballs',
'content': 'Combine ingredients, form into balls, brown, then place in oven.',
'date_posted': 'May 18th, 2022'
},
{
'author': 'Gina R.',
'title': 'Chicken Cutlets',
'content': 'Bread chicken, cook on each side for 8 min',
'date_posted': 'May 18th, 2022'
},
{
'author': 'Bella O.',
'title': 'Sub',
'content': 'Combine ingredients.',
'date_posted': 'May 18th, 2022'
}
]
# Create your views here.
def home(request):
context = {
'recipes': recipes
}
return render(request, 'recipes/home.html', context)
Now go back to the home page (run the server!) and you should see recipe data displaying 🍕🍟🍔
Step 4: Bootstrap styling
In this step we'll style the site so it looks a little better and has some navigation 🧭
1) Update the templates/recipes/base.html file so it loads the bootstrap css and js from a cdn, and has a basic navbar:
# templates/recipes/base.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
{% if title %}
<title>Django Recipes - {{title}}</title>
{% else %}
<title>Django Recipes</title>
{% endif %}
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0-beta1/dist/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-0evHe/X+R7YkIZDRvuzKMRqM+OrBnVFBL6DOitfPri4tjfHxaWutUpFmBp4vmVor"
crossorigin="anonymous"
/>
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container">
<a class="navbar-brand" href="{% url 'recipes-home' %}">Recipes App</a>
<button
class="navbar-toggler"
type="button"
data-bs-toggle="collapse"
data-bs-target="#navbarNav"
aria-controls="navbarNav"
aria-expanded="false"
aria-label="Toggle navigation"
>
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav">
<li class="nav-item">
<a
class="nav-link"
aria-current="page"
href="{% url 'recipes-home' %}"
>Recipes</a
>
</li>
</ul>
</div>
</div>
</nav>
<div class="container mt-4 col-8">{% block content %} {% endblock %}</div>
<script
src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0-beta1/dist/js/bootstrap.bundle.min.js"
integrity="sha384-pprn3073KE6tl6bjs2QrFaJGz5/SUsLqktiwsUTF55Jfv3qYSDhgCecCxMW52nD2"
crossorigin="anonymous"
></script>
<script
src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.5/dist/umd/popper.min.js"
integrity="sha384-Xe+8cL9oJa6tN/veChSP7q+mnSPaj5Bcu9mPX5F5xIGE0DVittaqT5lorf0EI7Vk"
crossorigin="anonymous"
></script>
<script
src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0-beta1/dist/js/bootstrap.min.js"
integrity="sha384-kjU+l4N0Yf4ZOJErLsIcvOU2qSb74wXpOhqTvwVx3OElZRweTnQ6d31fXEoRD1Jy"
crossorigin="anonymous"
></script>
</body>
</html>
2) Give the home.html some bootstrap styling so it looks nicer!
# templates/recipes/home.html
{% extends "recipes/base.html" %}
<!-- -->
{% block content %}
<h1>Recipes:</h1>
{% for recipe in recipes %}
<div class="card my-4">
<div class="card-body">
<h5 class="card-title">{{ recipe.title }}</h5>
<h6 class="card-subtitle mb-2 text-muted">{{ recipe.author }}</h6>
<p class="card-text">{{ recipe.content }}</p>
<h6 class="card-subtitle mb-2 text-muted">{{ recipe.date_posted }}</h6>
<a href="#" class="card-link">View Recipe</a>
</div>
</div>
{% endfor %}
<!-- -->
{% endblock content %}
Now we have some navigation and it looks a bit better 🦋
Step 5: Django Admin Setup
Django comes out-of-the-box with an admin interface to manipulate data, it's very convenient. By the end of this we'll setup a superuser that can edit data.
1) Stop the server and in the terminal let's create a superuser, but first we need to apply some pre-built migrations:
>> python manage.py migrate
2) Now we can create a superuser in the terminal (create your credentials when prompted):
>> python manage.py createsuperuser
3) Run the server and go to localhost:8000/admin and login with credentials from step 2
4) You should see a 'users' section where you'll find the superuser you created. You can click 'add' to create another user.
The django admin is a handy tool when developing and even when live to add/edit/update/delete data.
Step 6: Creating the database model
In this section we'll setup the database so data is stored and create some data via the admin ⚙️
1) Create a recipes model in the recipes/models.py file:
# recipes/models.py
from django.db import models
from django.contrib.auth.models import User
# Create your models here.
class Recipe(models.Model):
title = models.CharField(max_length=100)
description = models.TextField()
author = models.ForeignKey(User, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return self.title
2) Make the migrations to update the database, in the terminal run:
>> python manage.py makemigrations
>> python manage.py migrate
3) In order to see the model in the admin we need to register it, so in recipes/admin.py add:
# recipes/admin.py
from django.contrib import admin
from . import models
# Register your models here.
admin.site.register(models.Recipe)
4) Now run the server, head back to localhost:8000/admin, login and add some recipes
5) Let's use this real data in our template, so first update the recipes/views.py file home view to query the db:
# recipes/views.py
from . import models
def home(request):
recipes = models.Recipe.objects.all()
context = {
'recipes': recipes
}
return render(request, 'recipes/home.html', context)
def about(request):
return render(request, 'recipes/about.html', {'title': 'about page'})
6) And let's update the home.html template to use the attributes stored on the model:
# templates/recipes/home.html
{% extends "recipes/base.html" %}
<!-- -->
{% block content %}
<h1>Recipes:</h1>
{% for recipe in recipes %}
<div class="card my-4">
<div class="card-body">
<h5 class="card-title">{{ recipe.title }}</h5>
<h6 class="card-subtitle mb-2 text-muted">{{ recipe.author }}</h6>
<p class="card-text">{{ recipe.description }}</p>
<h6 class="card-subtitle mb-2 text-muted">
{{ recipe.updated_at|date:"F d, Y" }}
</h6>
<a href="#" class="card-link">View Recipe</a>
</div>
</div>
{% endfor %}
<!-- -->
{% endblock content %}
Amazing - now we have real data being stored and displayed on our site!
Step 7: User Registration 📝
In this step we'll make it so users can register on our site and create accounts!
1) Create a new Django app 'users' that will handle this functionality, in the terminal type:
>> python manage.py startapp users
2) Then add the app to our config/settings.py file:
# config/settings.py
INSTALLED_APPS = [
...
'django.contrib.staticfiles',
#local apps
'recipes',
'users',
]
3) Create a register view in the users/views.py file to handle new users registering (we'll create the form next):
# users/views.py
from django.shortcuts import render, redirect
from django.contrib.auth.forms import UserCreationForm
from django.contrib import messages
from . import forms
# Create your views here.
def register(request):
if request.method == "POST":
form = forms.UserRegisterForm(request.POST)
if form.is_valid():
form.save()
# cleaned data is a dictionary
username = form.cleaned_data.get('username')
messages.success(request, f"{username}, you're account is created!")
return redirect('recipes-home')
else:
form = forms.UserRegisterForm()
return render(request, 'users/register.html', {'form': form})
4) Now let's create the form that the view uses. in users/forms.py add:
#users/forms.py
from django import forms
from django.contrib.auth.models import User
from django.contrib.auth.forms import UserCreationForm
class UserRegisterForm(UserCreationForm):
email = forms.EmailField()
class Meta:
model = User
fields = ['username', 'email', 'password1', 'password2']
5) Setup a url pattern for registration that makes the view from step 1. We'll add this to the project urls, so in config/urls.py add:
# users/urls.py
...
from users import views as user_views
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('recipes.urls')),
path('register/', user_views.register, name="user-register"),
]
6) Create the template for the registration form, so under the users app create another directory at path 'users/templates/users' and add the file 'register.html' with the following markup:
# users/templates/users/register.html
{% extends "recipes/base.html" %}
<!-- -->
{% load crispy_forms_tags %}
<!-- -->
{% block content %}
<div class="container">
<form method="POST">
{% csrf_token %}
<fieldset class="form-group">
<legend class="border-bottom mb-4">Sign Up!</legend>
{{ form|crispy }}
</fieldset>
<div class="form-group py-3">
<input class="btn btn-outline-primary" type="submit" value="Sign Up" />
</div>
</form>
<div class="border-top pt-3">
<a class="text-muted" href="#">Already have an account? Log in.</a>
</div>
</div>
<!-- -->
{% endblock content %}
7) We used crispy forms (a 3rd party package) to make the forms look better so we need to install that, in the terminal type:
>> pip install django-crispy-forms
8) Add crispy forms to installed apps in the config/settings.py file and add another variable CRISPY_TEMPLATE_PACK.
# configs/settings.py file
INSTALLED_APPS = [
...
#local apps
'recipes',
'users',
# 3rd party
'crispy_forms',
]
...
CRISPY_TEMPLATE_PACK = 'bootstrap4'
Now new users can signup!
Step 8: Login and Logout
We'll finish up user auth in this step and users will be able to register, sign in, and sign out.
1) In the project urls add the built-in views for log in and log out. in 'config/urls.py' add (we'll also add a profile url that we'll add later on in this step:
# config/urls.py
from django.contrib import admin
from django.urls import path, include
from users import views as user_views
from django.contrib.auth import views as auth_views
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('recipes.urls')),
path('register/', user_views.register, name="user-register"),
# new urls
path('login/', auth_views.LoginView.as_view(template_name='users/login.html'), name="user-login"),
path('logout/', auth_views.LogoutView.as_view(template_name='users/logout.html'), name="user-logout"),
path('profile/', user_views.profile, name="user-profile"),
]
2) Let's create the templates we passed in the urls file above. So in users/templates/users create a login.html and logout.html file, in login.html add:
# users/templates/users/login.html
{% extends "recipes/base.html" %}
<!-- -->
{% load crispy_forms_tags %}
<!-- -->
{% block content %}
<div class="container">
<form method="POST">
{% csrf_token %}
<fieldset class="form-group">
<legend class="border-bottom mb-4">Log In!</legend>
{{ form|crispy }}
</fieldset>
<div class="form-group py-3">
<input class="btn btn-outline-primary" type="submit" value="Login" />
</div>
</form>
<div class="border-top pt-3">
<a class="text-muted" href="{% url 'user-register' %}"
>Don't have an account? Sign up.</a
>
</div>
</div>
<!-- -->
{% endblock content %}
3) And in logout.html add:
# users/templates/users/logout.html
{% extends "recipes/base.html" %}
<!-- -->
{% block content %}
<div class="container">
<h2>You have been logged out!</h2>
<div class="border-top pt-3">
<a class="text-muted" href="{% url 'user-login' %}">Log back in here.</a>
</div>
</div>
<!-- -->
{% endblock content %}
4) Update the project settings to redirect users before/after the login. So in config/settings.py add:
# config/settings.py
...
LOGIN_REDIRECT_URL = 'recipes-home'
LOGIN_URL = 'user-login'
5) Update the register view form the last step to redirect a user to login after registering. So in users/views.py update the register function (also create the profile view):
# users/views.py
from django.shortcuts import render, redirect
from django.contrib.auth.forms import UserCreationForm
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from . import forms
# Create your views here.
def register(request):
if request.method == "POST":
form = forms.UserRegisterForm(request.POST)
if form.is_valid():
form.save()
# cleaned data is a dictionary
username = form.cleaned_data.get('username')
messages.success(request, f"{username}, you're account is created, please login.")
return redirect('user-login')
else:
form = forms.UserRegisterForm()
return render(request, 'users/register.html', {'form': form})
@login_required()
def profile(request):
return render(request, 'users/profile.html')
6) Let's update the base.html navbar to link to these different actions based on if a user is logged in or now:
# recipes/templates/recipes/base.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
{% if title %}
<title>Django Recipes - {{title}}</title>
{% else %}
<title>Django Recipes</title>
{% endif %}
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0-beta1/dist/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-0evHe/X+R7YkIZDRvuzKMRqM+OrBnVFBL6DOitfPri4tjfHxaWutUpFmBp4vmVor"
crossorigin="anonymous"
/>
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container">
<a class="navbar-brand" href="{% url 'recipes-home' %}">Recipes App</a>
<button
class="navbar-toggler"
type="button"
data-bs-toggle="collapse"
data-bs-target="#navbarNav"
aria-controls="navbarNav"
aria-expanded="false"
aria-label="Toggle navigation"
>
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav">
<li class="nav-item">
<a
class="nav-link"
aria-current="page"
href="{% url 'recipes-home' %}"
>Recipes</a
>
</li>
</ul>
</div>
<div class="navbar-nav">
{% if user.is_authenticated %}
<a class="nav-item nav-link" href="{% url 'user-profile' %}"
>My Profile</a
>
<a class="nav-item nav-link" href="{% url 'user-logout' %}">Logout</a>
{% else %}
<a class="nav-item nav-link" href="{% url 'user-login' %}">Login</a>
<a class="nav-item nav-link" href="{% url 'user-register' %}"
>Register</a
>
{% endif %}
</div>
</div>
</nav>
<div class="container mt-4 col-8">
{% if messages %} {% for message in messages %}
<div class="alert alert-{{ message.tags }}">{{ message }}</div>
{% endfor %} {% endif %} {% block content %} {% endblock %}
</div>
<script
src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0-beta1/dist/js/bootstrap.bundle.min.js"
integrity="sha384-pprn3073KE6tl6bjs2QrFaJGz5/SUsLqktiwsUTF55Jfv3qYSDhgCecCxMW52nD2"
crossorigin="anonymous"
></script>
<script
src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.5/dist/umd/popper.min.js"
integrity="sha384-Xe+8cL9oJa6tN/veChSP7q+mnSPaj5Bcu9mPX5F5xIGE0DVittaqT5lorf0EI7Vk"
crossorigin="anonymous"
></script>
<script
src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0-beta1/dist/js/bootstrap.min.js"
integrity="sha384-kjU+l4N0Yf4ZOJErLsIcvOU2qSb74wXpOhqTvwVx3OElZRweTnQ6d31fXEoRD1Jy"
crossorigin="anonymous"
></script>
</body>
</html>
7) Finally let's create the profile template. so add a file users/templates/users/profile.html and add:
# users/templates/users/profile.html
{% extends "recipes/base.html" %}
<!-- -->
{% load crispy_forms_tags %}
<!-- -->
{% block content %}
<div class="container">
<h1>{{ user.username}}</h1>
</div>
<!-- -->
{% endblock content %}
Amazing! Now our app has full user auth, users can register, sign in, and sign out!
Step 9: Recipes CRUD:
Now we'll add the final functionality - letting users create, update, and delete recipes. This will make this a fully functioning web app! We'll also use class based views that speed up dev for common use cases.
1) Let's update the home page to use a class based view instead of the function. So updated recipes/views.py and add this class:
# recipes/views.py
from django.shortcuts import render
from django.views.generic import ListView
from . import models
class RecipeListView(ListView):
model = models.Recipe
template_name = 'recipes/home.html'
context_object_name = 'recipes'
2) Update the recipes url config to point to this class. so in recipes/urls.py update:
# recipes/urls.py
from django.urls import path
from . import views
urlpatterns = [
path('', views.RecipeListView.as_view(), name="recipes-home"),
]
3) Let's setup the detail view for each recipe, let's add this class to the recipes/views.py file:
# recipes/views.py
...
from django.views.generic import ListView, DetailView,
...
class RecipeDetailView(DetailView):
model = models.Recipe
4) And setup the url config to include the detail view (let's just add the create and delete as well):
# recipes/urls.py
from django.urls import path
from . import views
urlpatterns = [
path('', views.RecipeListView.as_view(), name="recipes-home"),
path('recipe/<int:pk>', views.RecipeDetailView.as_view(), name="recipes-detail"),
path('recipe/create', views.RecipeCreateView.as_view(), name="recipes-create"),
path('recipe/<int:pk>/update', views.RecipeUpdateView.as_view(), name="recipes-update"),
path('recipe/<int:pk>/delete', views.RecipeDeleteView.as_view(), name="recipes-delete"),
]
5) Let's create the expected template for this recipe detail page. create an html file 'recipes/templates/recipes/recipe_detail.html', and add the following markup:
# recipes/templates/recipes/reipe_detail.html
{% extends "recipes/base.html" %}
<!-- -->
{% block content %}
<h1>Recipe # {{object.id}}</h1>
<div class="card my-4">
<div class="card-body">
<h5 class="card-title">{{ object.title }}</h5>
<h6 class="card-subtitle mb-2 text-muted">{{ object.author }}</h6>
<p class="card-text">{{ object.description }}</p>
<h6 class="card-subtitle mb-2 text-muted">
{{ object.updated_at|date:"F d, Y" }}
</h6>
</div>
</div>
{% if object.author == user or user.is_staff %}
<div class="col-4">
<a class="btn btn-outline-info" href="{% url 'recipes-update' object.id %}">Update</a>
<a class="btn btn-outline-danger" href="{% url 'recipes-delete' object.id %}">Delete</a>
</div>
{% endif %}
<!-- -->
{% endblock content %}
6) Update the home page html template to link to the detail page for each recipe so in the recipes/templates/recipes/home.html page:
# recipes/templates/recipes/home.html
{% extends "recipes/base.html" %}
<!-- -->
{% block content %}
<h1>Recipes:</h1>
{% for recipe in recipes %}
<div class="card my-4">
<div class="card-body">
<h5 class="card-title">{{ recipe.title }}</h5>
<h6 class="card-subtitle mb-2 text-muted">{{ recipe.author }}</h6>
<p class="card-text">{{ recipe.description }}</p>
<h6 class="card-subtitle mb-2 text-muted">
{{ recipe.updated_at|date:"F d, Y" }}
</h6>
<a href="{% url 'recipes-detail' recipe.pk %}" class="card-link">View Recipe</a>
</div>
</div>
{% endfor %}
<!-- -->
{% endblock content %}
7) Let's create the views now for the create, update, and delete views in the recipes/views.py, so that file looks like:
# recipes/views.py
from django.shortcuts import render
from django.urls import reverse_lazy
from django.views.generic import ListView, DetailView, CreateView, UpdateView, DeleteView
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from . import models
class RecipeListView(ListView):
model = models.Recipe
template_name = 'recipes/home.html'
context_object_name = 'recipes'
class RecipeDetailView(DetailView):
model = models.Recipe
class RecipeDeleteView(LoginRequiredMixin, UserPassesTestMixin, DeleteView):
model = models.Recipe
success_url = reverse_lazy('recipes-home')
def test_func(self):
recipe = self.get_object()
return self.request.user == recipe.author
class RecipeCreateView(LoginRequiredMixin, CreateView):
model = models.Recipe
fields = ['title', 'description']
def form_valid(self, form):
form.instance.author = self.request.user
return super().form_valid(form)
class RecipeUpdateView(LoginRequiredMixin, UserPassesTestMixin, UpdateView):
model = models.Recipe
fields = ['title', 'description']
def test_func(self):
recipe = self.get_object()
return self.request.user == recipe.author
def form_valid(self, form):
form.instance.author = self.request.user
return super().form_valid(form)
8) Create the template for the create and update view, so create a file 'recipes/templates/recipes/recipe_form.html' with the following:
# recipes/templates/recipes/recipe_form.html
{% extends "recipes/base.html" %}
<!-- -->
{% load crispy_forms_tags %}
<!-- -->
{% block content %}
<div class="container">
<form method="POST">
{% csrf_token %}
<fieldset class="form-group">
<legend class="border-bottom mb-4">Add Recipe</legend>
{{ form|crispy }}
</fieldset>
<div class="form-group py-3">
<input class="btn btn-outline-primary" type="submit" value="Save" />
</div>
</form>
<div class="border-top pt-3">
<a class="text-muted" href="{% url 'recipes-home' %}"
>Cancel</a
>
</div>
</div>
<!-- -->
{% endblock content %}
9) Weneed to add a absolute url to the recipe model so django knows where to redirect to after creating or updating an object, so in recipes/model.py:
# recipes/models.py
from django.db import models
from django.contrib.auth.models import User
from django.urls import reverse
# Create your models here.
class Recipe(models.Model):
title = models.CharField(max_length=100)
description = models.TextField()
author = models.ForeignKey(User, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def get_absolute_url(self):
return reverse("recipes-detail", kwargs={"pk": self.pk})
def __str__(self):
return self.title
10) Now last thing is the delete template, so create a file 'recipes/templates/recipes/recipe_confirm_delete.html' and add this markup:
# recipes/templates/recipes/recipe_confirm_delete.html
{% extends "recipes/base.html" %}
<!-- -->
{% load crispy_forms_tags %}
<!-- -->
{% block content %}
<div class="container">
<form method="POST">
{% csrf_token %}
<fieldset class="form-group">
<legend class="mb-4">Are you sure you want to delete?</legend>
<h2>{{object}}</h2>
{{ form|crispy }}
</fieldset>
<div class="form-group py-3">
<input class="btn btn-outline-danger" type="submit" value="Delete" />
</div>
</form>
<div class="border-top pt-3">
<a class="text-muted" href="{% url 'recipes-home' %}"
>Cancel</a
>
</div>
</div>
<!-- -->
{% endblock content %}
11) Update the UI to link to these pages, we need to update the detail template and the base.html:
# template/recipes/recipe_detail.html
{% extends "recipes/base.html" %}
<!-- -->
{% block content %}
<h1>Recipe # {{object.id}}</h1>
<div class="card my-4">
<div class="card-body">
<h5 class="card-title">{{ object.title }}</h5>
<h6 class="card-subtitle mb-2 text-muted">{{ object.author }}</h6>
<p class="card-text">{{ object.description }}</p>
<h6 class="card-subtitle mb-2 text-muted">
{{ object.updated_at|date:"F d, Y" }}
</h6>
</div>
</div>
{% if object.author == user or user.is_staff %}
<div class="col-4">
<a class="btn btn-outline-info" href="{% url 'recipes-update' object.id %}">Update</a>
<a class="btn btn-outline-danger" href="{% url 'recipes-delete' object.id %}">Delete</a>
</div>
{% endif %}
<!-- -->
{% endblock content %}
And base.html
templates/recipes/base.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
{% if title %}
<title>Django Recipes - {{title}}</title>
{% else %}
<title>Django Recipes</title>
{% endif %}
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0-beta1/dist/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-0evHe/X+R7YkIZDRvuzKMRqM+OrBnVFBL6DOitfPri4tjfHxaWutUpFmBp4vmVor"
crossorigin="anonymous"
/>
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container">
<a class="navbar-brand" href="{% url 'recipes-home' %}">Recipes App</a>
<button
class="navbar-toggler"
type="button"
data-bs-toggle="collapse"
data-bs-target="#navbarNav"
aria-controls="navbarNav"
aria-expanded="false"
aria-label="Toggle navigation"
>
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav">
<li class="nav-item">
<a
class="nav-link"
aria-current="page"
href="{% url 'recipes-home' %}"
>Recipes</a
>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'recipes-about' %}">About</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'recipes-create' %}">Add Recipe</a>
</li>
</ul>
</div>
<div class="navbar-nav">
{% if user.is_authenticated %}
<a class="nav-item nav-link" href="{% url 'user-profile' %}"
>My Profile</a
>
<a class="nav-item nav-link" href="{% url 'user-logout' %}">Logout</a>
{% else %}
<a class="nav-item nav-link" href="{% url 'user-login' %}">Login</a>
<a class="nav-item nav-link" href="{% url 'user-register' %}"
>Register</a
>
{% endif %}
</div>
</div>
</nav>
<div class="container mt-4 col-8">
{% if messages %} {% for message in messages %}
<div class="alert alert-{{ message.tags }}">{{ message }}</div>
{% endfor %} {% endif %} {% block content %} {% endblock %}
</div>
<script
src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0-beta1/dist/js/bootstrap.bundle.min.js"
integrity="sha384-pprn3073KE6tl6bjs2QrFaJGz5/SUsLqktiwsUTF55Jfv3qYSDhgCecCxMW52nD2"
crossorigin="anonymous"
></script>
<script
src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.5/dist/umd/popper.min.js"
integrity="sha384-Xe+8cL9oJa6tN/veChSP7q+mnSPaj5Bcu9mPX5F5xIGE0DVittaqT5lorf0EI7Vk"
crossorigin="anonymous"
></script>
<script
src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0-beta1/dist/js/bootstrap.min.js"
integrity="sha384-kjU+l4N0Yf4ZOJErLsIcvOU2qSb74wXpOhqTvwVx3OElZRweTnQ6d31fXEoRD1Jy"
crossorigin="anonymous"
></script>
</body>
</html>
You made it! Now you have a sweet recipes app that you can customize to your liking or use to create something new!
Posted on June 16, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.