DEV Community

Cover image for Create a Recipe App in Django - Tutorial
Dom Vacchiano
Dom Vacchiano

Posted on

Create a Recipe App in Django - Tutorial

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:

Recipes Django App 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.

  1. 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

  2. Next we need to install Django, in your terminal type:
    >> pip install django==3.2.*

  3. 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 .

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

You made it! Now you have a sweet recipes app that you can customize to your liking or use to create something new!

Latest comments (1)

Collapse
 
fgriffin profile image
flg1973

Very well done tutorial, thank you for your time to make this and present it. One issue I did have was with the 'about' page it came up with an error, but I was lucky to find your repository on github to find the solution. Thanks again!