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
<iframe width="710" height="399" src="https://www.youtube.com/embed/w7EJu9Gd5Ns" allowfullscreen loading="lazy"> </iframe>
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
<iframe width="710" height="399" src="https://www.youtube.com/embed/FInbHmxAL6c" allowfullscreen loading="lazy"> </iframe>
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
<iframe width="710" height="399" src="https://www.youtube.com/embed/BFqvDCIKFNs" allowfullscreen loading="lazy"> </iframe>
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
<iframe width="710" height="399" src="https://www.youtube.com/embed/cTf-Hsqq3GA" allowfullscreen loading="lazy"> </iframe>
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
<iframe width="710" height="399" src="https://www.youtube.com/embed/zGCI948FoRU" allowfullscreen loading="lazy"> </iframe>
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
<iframe width="710" height="399" src="https://www.youtube.com/embed/8v1xUhwqkAc" allowfullscreen loading="lazy"> </iframe>
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 📝
<iframe width="710" height="399" src="https://www.youtube.com/embed/2WDgX6cxub4" allowfullscreen loading="lazy"> </iframe>
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
<iframe width="710" height="399" src="https://www.youtube.com/embed/qZDtAtHWwLk" allowfullscreen loading="lazy"> </iframe>
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:
<iframe width="710" height="399" src="https://www.youtube.com/embed/sUW-X1pvCz8?start=533" allowfullscreen loading="lazy"> </iframe>
<iframe width="710" height="399" src="https://www.youtube.com/embed/kLrSoPQq4Bk?start=2" allowfullscreen loading="lazy"> </iframe>
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>
所有评论(0)