Welcome to what I hope will be a very detailed and useful tutorial on building a Django web app from scratch to production. Having developed dozens of Django projects prior, I have acquired certain tips and tricks to increase efficiency in each Django project that I will present in the form of a tutorial. This tutorial is a step-by-step process of how I go about building robust Django applications. Enjoy! You can check out the deployment here: Live Link Demo: Part 1 🚀 For this sample project, we'll go a little beyond a simple Todo app or Blog site - we will build out a food recipe app with full user authentication, CRUD features, and deploy the app to live production on Heroku. The Directory The first step in any new project is setting up the directory. If you would like your Django project in a specific directory, navigate to it before running the startproject command. Create a new Django project with the following command: django-admin startproject [projectname] This should generate a file structure as such: ├─ foodanic (our sample project title) │ ├─ __init__ │ ├─ asgi │ ├─ settings │ ├─ urls │ ├─ wsgi ├─ manage.py .py .py .py .py .py Let's quickly add a folder titled into the directory with foodanic/ and manage.py templates The Environment The next crucial step is a virtual environment to contain all our dependencies in one module. To create a new virtual environment: virtualenv env Note: the [env] can be anything you want to name your virtual environment To activate the environment: env activate source /bin/ To deactivate the environment: deactivate After you create and activate the environment, an (env) tag will appear in your terminal next to your directory name. The Settings: This is a step you have to remember for all future projects because a proper initial settings setup will prevent bugs in the future. In your file, at the top, add next scroll down to the section and make the following change in : settings.py import os TEMPLATES DIRS os : [os.path.join(BASE_DIR, )], import 'DIRS' 'templates' This allows you to forward the root template of the project to the main templates directory, for future reference to the file. base.html While we're at it, let's install Django into our app with: pip install django Next, we will install a middleware that helps Heroku process images for Django applications called . whitenoise To install the dependency, run: pip install whitenoise Add whitenoise to your : MIDDLEWARE MIDDLEWARE = [ ... , ] # settings.py 'whitenoise.middleware.WhiteNoiseMiddleware' Every time we add a new dependency to the project, you'll want to freeze them to a file called . requirements.txt To do this run: pip freeze > requirements.txt Static and Media Static and media will serve the images on our app. Below the defined in the settings.py, add STATIC_URL STATIC_ROOT = os.path.join(BASE_DIR, ) STATIC_TMP = os.path.join(BASE_DIR, ) STATICFILES_DIRS = ( os.path.join(BASE_DIR, ), ) STATICFILES_STORAGE = MEDIA_URL = MEDIA_ROOT = os.path.join(BASE_DIR, ) os.makedirs(STATIC_TMP, exist_ok= ) os.makedirs(STATIC_ROOT, exist_ok= ) os.makedirs(MEDIA_ROOT, exist_ok= ) #settings.py 'staticfiles' 'static' 'static' 'whitenoise.storage.CompressedManifestStaticFilesStorage' '/media/' 'media' True True True This sets up our static and media directories in the most optimal way to serve our app. .gitignore Another important step in starting a Django project is the .gitignore file which will ignore the directories/files listed there. Create a .gitignore with: touch .gitignore Let's add the virtual environment we created to it so it doesn't take up extra cloud space on Github. # .gitignore env/ Part 2 🚲 Now that we have our project set up the way we want, let's begin by creating our first app to handle the logic. Along with that, let's also create a users app that we will use for User Authentication in a moment. Create a new app with: python manage.py startapp app python manage.py startapp users Add the app to settings.py: INSTALLED_APPS = [ , , ... ] # settings.py 'app' 'users' Now in order for our apps to be routed properly in our web app, we need to include our other apps in the main . foodanic/urls.py django.contrib admin django.urls path, include django.conf.urls.static static django.conf settings urlpatterns = [ path( , admin.site.urls), path( , include( )), path( , include( )), ] settings.DEBUG: urlpatterns += static(settings.MEDIA_URL, document_root = settings.MEDIA_ROOT) # foodanic/urls.py from import from import from import from import 'admin/' '' 'app.urls' 'u/' 'users.urls' if Apps Inside the new app and users directory, let's add two files and two folders. Files to add: Folders to add: ( each respectively) - urls.py - app/templates/app - forms.py - users/templates/users for The new app and users directory will look like this: ├─ app │ ├─ migrations/ | ├─ templates | | └── app/ │ ├─ __init__.py │ ├─ admin.py │ ├─ apps.py │ ├─ forms.py │ ├─ models.py │ ├─ tests.py │ ├─ urls.py │ └── views.py │ ├─ users │ ├─ migrations/ | ├─ templates | | └── users/ │ ├─ __init__.py │ ├─ admin.py │ ├─ apps.py │ ├─ forms.py │ ├─ models.py │ ├─ tests.py │ ├─ urls.py │ └── views.py User Authentication For our convenience, we will use the basic Django built-in authentication system. In our settings.py we will need to specify a login and logout redirect like so: ... LOGIN_REDIRECT_URL = LOGOUT_REDIRECT_URL = LOGIN_URL = # foodanic/settings.py '/' '/' 'login' In our newly created Users app, head to urls to include the Django auth views. django.urls path django.conf.urls.static static django.conf settings django.contrib.auth views auth_views .views * urlpatterns = [ path( , signup, name= ), path( , auth_views.LoginView.as_view(template_name= ), name= ), path( , auth_views.LogoutView.as_view(template_name= ), name= ), path( , auth_views.PasswordChangeView.as_view(template_name= ), name= ), path( , auth_views.PasswordChangeDoneView.as_view(template_name= ), name= ), path( , auth_views.PasswordResetView.as_view(template_name= , subject_template_name= , html_email_template_name= ), name= ), path( , auth_views.PasswordResetDoneView.as_view(template_name= ), name= ), path( , auth_views.PasswordResetConfirmView.as_view(template_name= ), name= ), path( , auth_views.PasswordResetCompleteView.as_view(template_name= ), name= ), ] settings.DEBUG: urlpatterns += static(settings.MEDIA_URL, document_root = settings.MEDIA_ROOT) # users/urls.py from import from import from import from import as from import 'signup/' 'signup' 'login/' 'users/login.html' 'login' 'logout/' 'users/logout.html' 'logout' 'change-password/' 'users/change-password.html' "change-password" 'password_change/done/' 'users/password_reset_done.html' 'password_change_done' 'password_reset/' 'users/forgot-password.html' 'users/password_reset_subject.txt' 'users/password_reset_email.html' 'password_reset' 'password_reset/done/' 'users/password_reset_done.html' 'password_reset_done' 'reset/<uidb64>/<token>/' 'users/password_reset_confirm.html' 'password_reset_confirm' 'reset/done/' 'users/password_reset_complete.html' 'password_reset_complete' if Now that was a ton of new urls we just added, let's make sure we have the templates needed. The line below will create all the necessary templates, given you're in the base directory (foodanic). touch users/templates/users/login.html && touch users/templates/users/logout.html && touch users/templates/users/change-password.html && touch users/templates/users/password_reset_done.html && touch users/templates/users/forgot-password.html && touch users/templates/users/password_reset_done.html && touch users/templates/users/password_reset_confirm.html && touch users/templates/users/password_reset_complete.html && touch users/templates/users/password_reset_email.html && touch users/templates/users/password_reset_subject.txt && touch users/templates/users/signup.html && touch users/templates/users/style.html Now we can setup each template to render from the base and display the corresponding form. Credit to for the bootstrap design. this Codepen users/style.html < > style , { : ; } { : ; : flex; : center; : center; } { : ; : ; : ; } { : ; } { : ; : ; } { : ; : ; } { :center; : ; } { :- ; : ; : ; } html body height 100% .global-container height 100% display align-items justify-content /* background-color: #f5f5f5; */ form padding-top 10px font-size 14px margin-top 30px .card-title font-weight 300 .btn font-size 14px margin-top 20px .login-form width 330px margin 20px .sign-up text-align padding 20px 0 0 .alert margin-bottom 30px font-size 13px margin-top 20px </ > style users/login.html {% extends 'base.html' %} {% block content %} {% load crispy_forms_tags %} Log in to Foodanic {% csrf_token %} Username Password Forgot password? Sign in Don't have an account? Create One {% include 'users/style.html' %} {% endblock %} <!-- users/login.html --> < = > br class "mt-0 mb-4" < = > div class "container" < = > div class "global-container" < = > div class "card login-form" < = > div class "card-body" < = > h3 class "card-title text-center" </ > h3 < = > div class "card-text" < = > form method "POST" < = > div class "form-group" < = > label for "username" </ > label < = = = = = > input type "text" name "username" class "form-control form-control-sm" id "username" aria-describedby "emailHelp" </ > div < = > div class "form-group" < = > label for "password" </ > label < = = > a href "{% url 'password_reset' %}" style "float:right;font-size:12px;text-decoration:none;" </ > a < = = = = > input type "password" name "password" class "form-control form-control-sm" id "password" </ > div < = = > button type "submit" class "btn btn-primary btn-block" </ > button < = > div class "sign-up" < = = > a href "{% url 'signup' %}" style "text-decoration:none;" </ > a </ > div </ > form </ > div </ > div </ > div </ > div </ > div users/logout.html {% extends 'base.html' %} {% block content %} You have successfully logged out of Foodanic. Log back in -> {% endblock %} <!-- users/logout.html --> < = > div class "container justify-content-center" < > h4 < = = > a href "{% url 'login' %}" style "text-decoration:none;" </ > a </ > h4 </ > div users/signup.html {% extends 'base.html' %} {% block content %} {% load crispy_forms_tags %} Signup for Foodanic {% csrf_token %} {{ form|crispy }} Sign Up Already have an account? Sign In {% include 'users/style.html' %} {% endblock %} <!-- users/signup.html --> < = > br class "mt-0 mb-4" < = > div class "container" < = > div class "global-container" < = > div class "card login-form" < = > div class "card-body" < = > h3 class "card-title text-center" </ > h3 < = > div class "card-text" < = > form method "POST" < = = > button type "submit" class "btn btn-primary btn-block" </ > button < = > div class "sign-up" < = = > a href "{% url 'login' %}" style "text-decoration:none;" </ > a </ > div </ > form </ > div </ > div </ > div </ > div </ > div users/change-password.html {% extends 'base.html' %} {% block content %} {% load crispy_forms_tags %} Log in to Foodanic {% csrf_token %} {{ form|crispy }} Update Password {% include 'users/style.html' %} {% endblock %} <!-- users/change-password.html --> < = > br class "mt-0 mb-4" < = > div class "container" < = > div class "global-container" < = > div class "card login-form" < = > div class "card-body" < = > h3 class "card-title text-center" </ > h3 < = > div class "card-text" < = > form method "POST" < = = > button type "submit" class "btn btn-primary btn-block" </ > button </ > form </ > div </ > div </ > div </ > div </ > div users/password_reset_done.html {% extends 'base.html' %} {% block title %}Email Sent{% endblock %} {% block content %} Check your inbox. We've emailed you instructions for setting your password. You should receive the email shortly! Return Home {% endblock %} <!-- users/password_reset_done.html --> < > br < > br < = > div class "container" < > h1 </ > h1 < > p </ > p < = > button class "btn btn-primary" < = > a href "{% url 'home' %}" </ > button </ > a </ > div users/forgot-password.html {% extends 'base.html' %} {% block content %} {% load static %} {% load crispy_forms_tags %} Forgot Your Password? We get it, stuff happens. Just enter your email address below and we'll send you a link to reset your password! {% csrf_token %} Reset Password Create an Account! Already have an account? Login! {% endblock %} <!-- users/forgot-password.html --> < = > body class "bg-gradient-primary" < = > div class "container" < = > div class "row justify-content-center" < = > div class "col-xl-10 col-lg-12 col-md-9" < = > div class "card o-hidden border-0 shadow-lg my-5" < = > div class "card-body p-0" < = > div class "row" < = > div class "col-lg-6 d-none d-lg-block bg-password-image" < = = = > img src "https://i.imgur.com/ryKdO1v.jpg" style "width: 100%; height: 100%;" alt "" </ > div < = > div class "col-lg-6" < = > div class "p-5" < = > div class "text-center" < = > h1 class "h4 text-gray-900 mb-2" </ > h1 < = > p class "mb-4" </ > p </ > div < = = > form class "user" method "POST" < = = > div class "form-group" style "border: 2px gray;" <!-- {{ form|crispy }} --> < = = = = = = > input type "email" name "email" class "form-control form-control-user" id "exampleInputEmail" aria-describedby "emailHelp" placeholder "Enter your email..." </ > div < > br < = = = > button class "btn btn-primary btn-user btn-block" type "submit" style "text-decoration: none;" </ > button </ > form < > hr < = > div class "text-center" < = = = > a class "small" href "{% url 'signup' %}" style "text-decoration: none;" </ > a </ > div < = > div class "text-center" < = = = > a class "small" href "{% url 'login' %}" style "text-decoration: none;" </ > a </ > div </ > div </ > div </ > div </ > div </ > div </ > div </ > div </ > div users/password_reset_subject.txt Foodanic Password Reset users/password_reset_email.html {% autoescape off %} Hi, {{ user.username }}. We received a request for a password reset. If this was you, follow the link below to reset your password. If this wasn't you, no action is needed. {{ protocol }}://{{ domain }}{% url 'password_reset_confirm' uidb64=uid token=token %} If clicking the link above doesn't work, please copy and paste the URL in a new browser window instead. Sincerely, Foodanic {% endautoescape %} <!-- users/password_reset_email.html --> < > br < > br < > br < > br < = = > a href "{{ protocol }}://{{ domain }}{% url 'password_reset_confirm' uidb64=uid token=token %}" target "_blank" </ > a < > br < > br < > br < > br < > br users/password_reset_done.html {% extends 'base.html' %} {% block title %}Email Sent{% endblock %} {% block content %} Check your inbox. We've emailed you instructions for setting your password. You should receive the email shortly! Return Home {% endblock %} < > br < > br < = > div class "container" < > h1 </ > h1 < > p </ > p < = > button class "btn btn-primary" < = > a href "{% url 'home' %}" </ > button </ > a </ > div password_reset_confirm.html {% extends 'base.html' %} {% block title %}Enter new password{% endblock %} {% load crispy_forms_tags %} {% block content %} {% if validlink %} Set a new password {% csrf_token %} {{ form|crispy }} Change my password {% else %} The password reset link was invalid, possibly because it has already been used. Please request a new password reset. {% endif %} {% endblock %} < > br < > br < = > div class "container" < > h1 </ > h1 < = > form method "POST" < > br < = = > button class "btn btn-primary" type "submit" </ > button </ > form </ > div < > p </ > p users/password_reset_complete.html {% extends 'base.html' %} {% block title %}Password reset complete{% endblock %} {% block content %} Password reset complete Your new password has been set. You can now log in . {% endblock %} < > br < > br < = > div class "container" < > h1 </ > h1 < > p < = = > a href "{% url 'login' %}" style "text-decoration: none;" </ > a </ > p </ > div Now you can give our new user authentication a try, which will take you on a tour of the Django Authentication System. Keep in mind, the Password Reset won't work because we didn't set up the email server with Django. I recommend for assisting you with email reset setup. this tutorial If you would like to create an admin account for your site, you can do so with: python manage.py createsuperuser Main App Here is where the fun part comes, we will build out the CRUD operations of the app. Views The views control the logic of the app, rendering out functions and performing necessary operations on forms, templates, and anything else having to do with your app. First, we will write out the functions we will be working on. django.shortcuts render context = {} render(request, , context) context = {} render(request, , context) context = {} render(request, , context) context = {} render(request, , context) context = {} render(request, , context) # views.py from import : def home (request) return 'app/index.html' : def detail (request, id) return 'app/detail.html' : def create (request) return 'app/create.html' : def update (request, id) return 'app/update.html' : def delete (request, id) return 'app/delete.html' Next, let's add them to the file in app in addition to the media url and root to handle our future images: urls.py django.urls path .views * django.conf.urls.static static django.conf settings urlpatterns = [ path( , home, name= ), path( , detail, name= ), path( , create, name= ), path( , update, name= ), path( , delete, name= ), ] settings.DEBUG: urlpatterns += static(settings.MEDIA_URL, document_root = settings.MEDIA_ROOT) # app/urls.py from import from import from import from import '' 'home' 'detail/<int:id>/' 'detail' 'new/' 'create' 'update/<int:id>/' 'update' 'delete/<int:id>/' 'delete' if In order for our urls in app to work properly, we need to add them to our main urls.py file. In addition, also add the media url and root to the main urls as well. django.contrib admin django.urls path, include django.conf.urls.static static django.conf settings urlpatterns = [ path( , admin.site.urls), path( , include( )), path( , include( )), ] settings.DEBUG: urlpatterns += static(settings.MEDIA_URL, document_root = settings.MEDIA_ROOT) # foodanic/urls.py from import from import from import from import 'admin/' '' 'app.urls' 'u/' 'users.urls' if Run migrations and server Now we are ready to start developing our web application. Let's run migrations to create an initial database and run our app. Run migrations with: python manage.py migrate Run server with: python manage.py runserver [OPTIONAL: PORT] *Note: The optional port can be used as such: python manage.py runserver 8000 python manage.py runserver 1234 Model Now we can set up our model that will store each recipe. In models.py add the following code: django.db models datetime datetime, timedelta markdownx.models MarkdownxField django.contrib.auth.models User name = models.CharField(max_length= ) description = models.TextField() prep = models.CharField(max_length= ) cook = models.CharField(max_length= ) servings = models.IntegerField(default= , null= , blank= ) image = models.ImageField(upload_to= ) ingredients = MarkdownxField() directions = MarkdownxField() notes = models.TextField(null= , blank= ) author = models.ForeignKey(User, on_delete=models.CASCADE) self.name markdownify(self.ingredients) markdownify(self.directions) # models.py from import from import from import from import : class Recipe (models.Model) 255 255 255 1 True True 'media/' True True : def __str__ (self) return @property : def formatted_ingredients (self) return @property : def formatted_directions (self) return A few things to note here: we have 9 fields that will hold the information for the Recipe model. We will be using Django MarkdownX for two of the fields for a nicer look. The creates a property tag we can use in our templates to render out Markdown fields. (Github Link) @property To install Django Markdown, run: pip install django-markdownx Add it to settings.py: INSTALLED_APPS = [ , ... ] # settings.py 'markdownx' Add it to requirements with: pip freeze > requirements.txt Add it to main urls.py: urlpatterns = [ path( , include( )), ] # foodanic/urls.py 'markdownx/' 'markdownx.urls' Now that we have our model set up, we can go ahead and run migrations. Note: You must run migrations with every change you make to models so the database gets updated. python manage.py makemigrations && python manage.py migrate If everything went well, you should see an output similar to this and a brand new migration in your app folder. migrations/ (env) ➜ foodanic git:(master) ✗ python manage.py makemigrations && python manage.py migrate Migrations : app/migrations/0001_initial.py - Create model Recipe Operations to perform: Apply all migrations: admin, app, auth, contenttypes, sessions Running migrations: Applying app.0001_initial... OK (env) ➜ foodanic git:(master) ✗ for 'app' In order for our model to successfully be displayed in the Django Admin, we need to register it to the admin.py file like so: django.contrib admin .models * admin.site.register(Recipe) # app/admin.py from import from import Main Form In order to pass data into our database swiftly, we will use a django ModelForm based on our model. Make the following changes in your forms file: django forms .models * durationwidget.widgets TimeDurationWidget prep = forms.DurationField(widget=TimeDurationWidget(show_days= , show_hours= , show_minutes= , show_seconds= ), required= ) cook = forms.DurationField(widget=TimeDurationWidget(show_days= , show_hours= , show_minutes= , show_seconds= ), required= ) model = Recipe fields = exclude = ( ,) # app/forms.py from import from import from import : class RecipeForm (forms.ModelForm) False True True False False False True True False False : class Meta '__all__' 'author' With this form, we will be able to render out all the fields in the Recipe model. Additionally, if you wanted to only include certain fields you would list them in an array as such: or if you wanted to exclude certain fields, you would list them as such: . fields = ['name', 'image',] exclude = ('name', 'image',) You might have noticed we added a new library to help us render the duration field for prep and cook time. Also, let's install another module we will use later to help us with the form, . Django Crispy Forms Install it with pip: pip install django-durationwidget pip install django-crispy-forms Add it to settings: INSTALLED_APPS = [ , , ] TEMPLATES = [ : , ] CRISPY_TEMPLATE_PACK = # settings.py 'durationwidget' 'crispy_forms' 'APP_DIRS' True # set to True # on the bottom of settings.py 'bootstrap4' And let's freeze the requirements to save the dependency: pip freeze > requirements.txt CRUD Now we are ready to start writing the logic of our views. Let's begin with the __C__ in __CRUD__ which stands for (Create, Read, Update, Delete) Create In our views, let's import the forms, the models and render out the form for a GET and POST request. The GET request will render when a user goes on the page to create a new recipe whereas the POST will handle the form logic once submitted. django.shortcuts render, redirect, get_object_or_404 django.contrib.auth.decorators login_required django.core.files.storage FileSystemStorage datetime datetime, timedelta .models * .forms * context = {} request.method == : form = RecipeForm() context[ ] = RecipeForm() render(request, , context) request.method == request.FILES != : form = RecipeForm(request.POST, request.FILES) form.is_valid(): new = Recipe() user = request.user new.author = user new.name = form[ ].value() new.description = form[ ].value() new.prep = form[ ].value() new.cook = form[ ].value() new.servings = form[ ].value() new.ingredients = form[ ].value() new.directions = form[ ].value() new.notes = form[ ].value() theimg = request.FILES[ ] fs = FileSystemStorage() filename = fs.save(theimg.name, theimg) file_url = fs.url(filename) new.image = filename new.save() redirect( ) : form = RecipeForm() context[ ] = RecipeForm() render(request, , context) render(request, , context) # app/views.py from import from import from import from import from import from import @login_required : def create (request) if 'GET' 'form' return 'app/create.html' elif 'POST' and None if 'name' 'description' 'prep' 'cook' 'servings' 'ingredients' 'directions' 'notes' 'image' return 'home' else 'form' return 'app/create.html' return 'app/create.html' Woah, that's a whole lotta code - let's break it down so we understand what it's doing. The if statement handles the logic of which template to render if GET and where to redirect the user after the submission, POST. The in the form is for our image field. Essentially, if the form submitted passes our parameters, we create a new instance of the Recipe model and save the contents of the form to the model values respectively. request.FILES Now we have to render out a template for the form. To do that, we will need to create a file in our base templates. I'll add the most up-to-date version of Bootstrap which is 5 - so if you're reading this tutorial later on, be sure to update the corresponding CDN for Bootstrap, found at . base.html getbootstrap.com foodanic/templates/base.html Foodanic {% block content %} {% endblock %} <!DOCTYPE html> < = > html lang "en" < > head < = > meta charset "UTF-8" < = = > meta name "viewport" content "width=device-width, initial-scale=1.0" < = = > meta http-equiv "X-UA-Compatible" content "ie=edge" < > title </ > title < = = = = > link href "https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta2/dist/css/bootstrap.min.css" rel "stylesheet" integrity "sha384-BmbxuPwQa2lc/FVzBcNJ7UAyJxM6wuqIj61tLrc4wSX0szH/Ev+nYRRuWlolflfl" crossorigin "anonymous" < = = = > link rel "shortcut icon" href "https://media.istockphoto.com/vectors/hand-opening-silver-cloche-vector-id1135322593?k=6&m=1135322593&s=612x612&w=0&h=QhIjVZdKyGzfQ6aGojvSFgXpLZpEG7RsueYSLngbdLA=" type "image/x-icon" </ > head < > body < = = = > script src "https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta2/dist/js/bootstrap.bundle.min.js" integrity "sha384-b5kHyXgcpbZJO/tY9Ul7kGkf1S0CWuKcCD38l8YkeH8z8QjE0GmW1gYU5S9FOnJ0" crossorigin "anonymous" </ > script </ > body </ > html Now we have our base.html setup, and we can render other templates without the unnecessary content. I Bootstrapped the page to a passable format but feel free to change up the design however you please. create.html app/create.html {% extends 'base.html' %} {% block content %} {% load crispy_forms_tags %} New Recipe Note: The Ingredients and Directions fields are Markdown Supported. Learn more about markdown here . {% csrf_token %} {{ form.name|as_crispy_field }} {{ form.image|as_crispy_field }} {{ form.description|as_crispy_field }} {{ form.prep|as_crispy_field }} {{ form.cook|as_crispy_field }} {{ form.servings|as_crispy_field }} {{ form.ingredients|as_crispy_field }} {{ form.directions|as_crispy_field }} {{ form.notes|as_crispy_field }} Post Recipe {{ form.media }} {% endblock %} < = > br class "mt-0 mb-4" < = > div class "container" < > h4 </ > h4 < > p < > i < = = = > a href "https://www.markdownguide.org/cheat-sheet/" target "_blank" style "text-decoration: none;" </ > a </ > i </ > p < > br < = = > form method "post" enctype "multipart/form-data" < = > div class "row" < = > div class "col-6" < = > div class "col" </ > div </ > div < = > div class "col-6" </ > div </ > div < > br < = > div class "row justify-content-center" < = > div class "col-2" </ > div < = > div class "col-2" </ > div < = > div class "col-2" </ > div </ > div < > br < = > div class "row" < = > div class "col-4" </ > div < = > div class "col-4" </ > div < = > div class "col-4" </ > div </ > div < = > div class "mt-4 mb-4 d-flex justify-content-center" < = = > button type "submit" class "btn btn-success" </ > button </ > div </ > form </ > div In the beginning, you can see we render the info in block content tags based on the file we created. We load crispy in with the tag and set each field as a crispy field. The tag renders out the content for the MarkdownX fields. Alternatively, you can render out the entire form as crispy like this: . base.html {{ form.media }} {{ form|crispy }} The new route should look something like this: Read The read portion of CRUD has to do with being able to view each individual object in our database. First, we'll do single recipes, then we'll pull the entire set for our index page. app/views.py django.shortcuts render, redirect, get_object_or_404 django.contrib.auth.decorators login_required django.core.files.storage FileSystemStorage datetime datetime, timedelta markdownx.utils markdownify .models * .forms * recipes = Recipe.objects.all() context = { : recipes, } render(request, , context) recipe = get_object_or_404(Recipe, id=id) recipe.ingredients = markdownify(recipe.ingredients) recipe.directions = markdownify(recipe.directions) context = { : recipe, } render(request, , context) from import from import from import from import from import # new from import from import : def home (request) 'recipes' return 'app/index.html' : def detail (request, id) 'recipe' return 'app/detail.html' The template below is credited to user on Codepen. ARIELOZAM app/detail.html {% extends 'base.html' %} {% block content %} {% load crispy_forms_tags %} Back Home {{ recipe.name|capfirst }} Recipe {% if user.is_authenticated and request.user == recipe.author %} {% endif %} by {{recipe.author}} Description: {{recipe.description}} Quick Info Ingredients Directions Servings: {{ recipe.servings }} Prep: {{ recipe.prep }} Cook: {{ recipe.cook }} {{ recipe.ingredients|safe }} {{ recipe.directions|safe }} Are you 100% sure? Are you absolutely sure you want to delete the {{recipe.name|capfirst}} Recipe? The data will be erased from the database and will not be retrievable. Nevermind OK, Proceed {% endblock %} < = > br class "mt-0 mb-4" < = > div class "container" < = > div class "bg-codeblocks" < = > div class "main-box-codeblocks" < = > div class "container" < = > div class "row" < = > div class "col-md-12" < = > a href "{% url 'home' %}" < = > button class "btn btn-info mb-4" </ > button </ > a </ > div </ > div < = > div class "row" < = > div class "col-md-6" < = > div class "box-image-codeblocks" < = > div class "swiper-container gallery-top" < = > div class "swiper-wrapper" < = > div class "swiper-slide" < = > div class "product-image" < = = = = > img src "{{recipe.image.url}}" alt "{{recipe.name}}" class "img-fluid" style "width: 650px; height: 100%;" </ > div </ > div </ > div </ > div </ > div </ > div < = > div class "col-md-6" < = > h2 class "text-bold text-strong" < = > a href "{% url 'update' recipe.id %}" < = > i class "fas fa-edit" </ > i </ > a < = = > span data-bs-toggle "modal" data-bs-target "#delete" < = > i class "fas fa-trash" </ > i </ > span </ > h2 < = > span class "seller-name-codeblocks" < > h5 < = = > a href "#" style "text-decoration: none;" </ > a </ > h5 </ > span < > br < = > span class "description-codeblocks" < > p < > strong </ > strong < > br < = > span class "text-muted" < = > p style "width: 450px;overflow:scroll;" </ > p </ > span </ > p </ > span < > br < = > span class "extras-codeblocks " < = = = > ul class "nav nav-tabs my-2" id "myTab" role "tablist" < = = > li class "nav-item" role "presentation" < = = = = = = = > a class "nav-link active" id "home-tab" data-toggle "tab" href "#home" role "tab" aria-controls "home" aria-selected "true" </ > a </ > li < = = > li class "nav-item" role "presentation" < = = = = = = = > a class "nav-link" id "ingredients-tab" data-toggle "tab" href "#ingredients" role "tab" aria-controls "ingredients" aria-selected "false" </ > a </ > li < = = > li class "nav-item" role "presentation" < = = = = = = = > a class "nav-link" id "directions-tab" data-toggle "tab" href "#directions" role "tab" aria-controls "directions" aria-selected "false" </ > a </ > li </ > ul < = = > div class "tab-content" id "myTabContent" < = = = = > div class "tab-pane fade show active" id "home" role "tabpanel" aria-labelledby "home-tab" < > br < = > table style "width:250px;" < > tr < > th </ > th < > td </ > td </ > tr < > tr < > th </ > th < > td </ > td </ > tr < > tr < > th </ > th < > td </ > td </ > tr </ > table </ > div < = = = = > div class "tab-pane fade" id "ingredients" role "tabpanel" aria-labelledby "ingredients-tab" </ > div < = = = = > div class "tab-pane fade" id "directions" role "tabpanel" aria-labelledby "directions-tab" </ > div </ > div </ > span </ > div </ > div </ > div </ > div </ > div </ > div <!-- Modal --> < = = = = = = = > div class "modal fade" id "delete" data-bs-backdrop "static" data-bs-keyboard "false" tabindex "-1" aria-labelledby "deleteLabel" aria-hidden "true" < = > div class "modal-dialog" < = > div class "modal-content" < = > div class "modal-header" < = = > h5 class "modal-title" id "deleteLabel" </ > h5 < = = = = > button type "button" class "btn-close" data-bs-dismiss "modal" aria-label "Close" </ > button </ > div < = > div class "modal-body" </ > div < = > div class "modal-footer" < = = = > button type "button" class "btn btn-secondary" data-bs-dismiss "modal" </ > button < = > a href "{% url 'delete' recipe.id %}" < = = > button type "button" class "btn btn-primary" </ > button </ > a </ > div </ > div </ > div </ > div < > style { : ; : absolute; : ; : (to right, #4A00E0, #8E2DE2); : (to right, #4A00E0, #8E2DE2); : auto; } { : ; : ; : ; : ; : auto; : relative; : block; : (0,0,0,0.5); : auto; } .bg-codeblocks margin-top 4% position background #8E2DE2 background -webkit-linear-gradient background linear-gradient height .main-box-codeblocks background-color #FAFAFA border-radius 20px padding 5em 2em width 90% height position display box-shadow 0 0px 20px 2px rgba margin 3em </ > style < = > script src "https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js" </ > script < = > script src "https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.5.3/js/bootstrap.min.js" </ > script < = = > link rel "stylesheet" href "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.2/css/all.min.css" The new template should look like this: Now it's time to get all of our Recipes to display on our homepage. app/index.html {% extends 'base.html' %} {% block content %} {% load crispy_forms_tags %} Foodanic All Recipes New Recipe {% if not request.user.is_authenticated %} Login {% else %} Logout {% endif %} A Warm Welcome! Browse through our collection of various recipes. Post Your Recipe {% for recipe in recipes %} {{recipe.name}} Recipe {{recipe.description|truncatechars:65}} Prep Time: {{recipe.prep}} Cook Time: {{recipe.cook}} View {% endfor %} Copyright &copy; Foodanic 2021 {% endblock %} < = > nav class "navbar navbar-expand-lg navbar-light bg-light" < = > div class "container-fluid" < = = > a class "navbar-brand" href "{% url 'home' %}" </ > a < = = = = = = = > button class "navbar-toggler" type "button" data-bs-toggle "collapse" data-bs-target "#navbarSupportedContent" aria-controls "navbarSupportedContent" aria-expanded "false" aria-label "Toggle navigation" < = > span class "navbar-toggler-icon" </ > span </ > button < = = > div class "collapse navbar-collapse" id "navbarSupportedContent" < = > ul class "navbar-nav me-auto mb-2 mb-lg-0" < = > li class "nav-item" < = = = > a class "nav-link active" aria-current "page" href "{% url 'home' %}" < = = > button class "btn btn-warning" style "color: white;" </ > button </ > a </ > li < = > li class "nav-item" < = = > a class "nav-link active" href "{% url 'create' %}" < = = > button class "btn btn-info" style "color: white;" </ > button </ > a </ > li </ > ul < = = = > a class "nav-link active" aria-current "page" href "{% url 'login' %}" < = = > button class "btn btn-dark" style "color: white;" </ > button </ > a < = = = > a class "nav-link active" aria-current "page" href "{% url 'logout' %}" < = = > button class "btn btn-dark" style "color: white;" </ > button </ > a </ > div </ > div </ > nav < = > div class "container" < = > header class "jumbotron my-4" < = > h1 class "display-3" </ > h1 < = > p class "lead" </ > p < = > a href "{% url 'create' %}" < = = > button class "btn btn-info btn-lg" style "color: white;" </ > button </ > a </ > header < > br < = > div class "row text-center" < = > div class "col-lg-4 col-md-6 mb-4" < = > div class "card h-100 w-75" < = > a href "{% url 'detail' recipe.id %}" < = = = > img class "card-img-top" src "{{recipe.image.url}}" alt "{{recipe.name}}" </ > a < = > div class "card-body" < = > h4 class "card-title" < = = > a href "{% url 'detail' recipe.id %}" style "text-decoration: none;" </ > a </ > h4 < = > p class "card-text" </ > p < > p < > b </ > b < > br < > b </ > b </ > p </ > div < = > div class "card-footer" < = = > a href "{% url 'detail' recipe.id %}" class "btn btn-primary" </ > a </ > div </ > div </ > div </ > div </ > div < > br < > br < > br < = > footer class "py-5 bg-dark" < = > div class "container" < = > p class "m-0 text-center text-white" </ > p </ > div </ > footer With a few sample Recipes, here is how the Home Page comes out to: Update The update view will grab an instance of the object we want to update and save the new information. app/views.py recipe = get_object_or_404(Recipe, id=id) context = { : recipe } request.method == : form = RecipeForm(instance=recipe) context[ ] = form render(request, , context) request.method == request.FILES != : form = RecipeForm(request.POST, request.FILES, instance=recipe) form.is_valid(): form.save() redirect( , recipe.id) : form = RecipeForm(instance=recipe) context[ ] = form render(request, , context) render(request, , context) @login_required : def update (request, id) 'recipe' if 'GET' 'form' return 'app/update.html' elif 'POST' and None if return 'detail' else 'form' return 'app/update.html' return 'app/update.html' A short and simple route rendering a replica of the create view except we're saving the form generically. app/update.html {% extends 'base.html' %} {% block content %} {% load crispy_forms_tags %} Update Recipe Note: The Ingredients and Directions fields are Markdown Supported. Learn more about markdown here . {% csrf_token %} {{ form.name|as_crispy_field }} {{ form.image|as_crispy_field }} {{ form.description|as_crispy_field }} {{ form.prep|as_crispy_field }} {{ form.cook|as_crispy_field }} {{ form.servings|as_crispy_field }} {{ form.ingredients|as_crispy_field }} {{ form.directions|as_crispy_field }} {{ form.notes|as_crispy_field }} Save Recipe {{ form.media }} {% endblock %} < = > br class "mt-0 mb-4" < = > div class "container" < > h4 </ > h4 < > p < > i < = = = > a href "https://www.markdownguide.org/cheat-sheet/" target "_blank" style "text-decoration: none;" </ > a </ > i </ > p < > br < = = > form method "post" enctype "multipart/form-data" < = > div class "row" < = > div class "col-6" < = > div class "col" </ > div </ > div < = > div class "col-6" </ > div </ > div < > br < = > div class "row justify-content-center" < = > div class "col-2" </ > div < = > div class "col-2" </ > div < = > div class "col-2" </ > div </ > div < > br < = > div class "row" < = > div class "col-4" </ > div < = > div class "col-4" </ > div < = > div class "col-4" </ > div </ > div < = > div class "mt-4 mb-4 d-flex justify-content-center" < = = > button type "submit" class "btn btn-success" </ > button </ > div </ > form </ > div Go ahead and give it a try, your form should be showing you the data of the object and saving it properly to the database. Delete As much as it hurts deleting our database objects, sometimes it's what the user or we, want to do. app/views.py recipe = get_object_or_404(Recipe, id=id) request.user == recipe.author: redirect( , recipe.id) : name = recipe.name recipe.delete() context = { : name } render(request, , context) @login_required : def delete (request, id) if not return 'detail' else 'name' return 'app/delete.html' app/delete.html {% extends 'base.html' %} {% block content %} {% load crispy_forms_tags %} You have successfully deleted the {{name|capfirst}} Recipe Back Home New Recipe {% endblock %} < = > br class "mt-0 mb-4" < = > div class "container" < > h4 </ > h4 < > br < > br < = > div class "row" < = > div class "col" < = > a href "{% url 'home' %}" < = > button class "btn btn-primary" </ > button </ > a </ > div < = > div class "col" < = > a href "{% url 'create' %}" < = > button class "btn btn-success" </ > button </ > a </ > div </ > div </ > div Part 3 🚗 We are now ready for our live deployment to Heroku. At this point, please head over to , create and push your code to a new repository so we can host it on Heroku. Also, if you don't already have a Heroku account, head to to create one. Github Heroku Home Additionally, Heroku needs gunicorn to run so we'll install it with pip. pip install gunicorn pip freeze > requirements.txt Next, we need a so Heroku knows to run our app with gunicorn. Procfile web: gunicorn foodanic.wsgi -- -file - log Make sure you are logged into Heroku in your terminal with: heroku login Create a new Heroku app with: heroku create heroku create [app-name] # or After you git and commit, run git push heroku HEAD:master When you get a success message saying your app (with link) was deployed to Heroku, we have to ensure that our app accepts that reference. foodanic/settings.py ALLOWED_HOSTS = [ ] '[your-app].herokuapp.com' Be sure to replace with the corresponding app name for your Heroku app. ['your-app'] Then redo the commit and... git push heroku HEAD:master The End If you have reached the end of this tutorial, you are awesome! If you encountered any bugs/errors throughout the tutorial, please don't be shy to post them in the comments so I can fix them quickly before anyone else experiences them. Programming is a collaborative effort after all 😁 Project Links: Github Repo: Link Live: Link Resources: Detail Page Codepen: Link User Auth Page Codepen: Link Home Page Bootstrap Template: Link Django MarkdownX: Link Django Crispy Forms: Link 🙂 P.S. If there are certain projects/topics you'd like me to dive into, drop them in the comments below and I will do my best to research it. Thanks for reading Also published on: https://medium.com/analytics-vidhya/how-to-build-a-django-web-app-from-scratch-tutorial-20034f0a3043