Build a Photo-sharing App with Django

    Daniel Diaz

    Django is the most-used Python framework for web development. Its built-in features and robust structure make it an excellent option when building web applications. But there are so many resources out there that it’s sometimes overwhelming to apply that knowledge to real-world projects. In this tutorial, we’re going to build a full-stack web application, using Django on the back end and Django Templates stylized with Bootstrap on the front end.


    To get the most out of this tutorial, you’d ideally have a grasp of the following:

    • the basics of Python
    • object-oriented programming in Python
    • the basics of the Django web framework

    If you don’t have previous experience with Django, don’t be afraid of continuing with this tutorial. This will be a step-by-step process, and each step will be explained.

    Before starting, I want to introduce you to your new best ally, the Django documentation. We’ll be referencing it throughout the article, so make sure to get acquainted with it.

    A Django Photo-sharing App

    All the source code of this tutorial is available on this GitHub repo.

    The complexity of a project depends on all the features we want to include. The more features we want to offer to users, the more time we’ll need to spend building and integrating everything into a unique project.

    Taking that into account, we’re going to see a quick distinction between what we’re going to build and what we’re not.

    What we’re going to build

    In this tutorial, we’ll build a full-stack (back-end and front-end development) photo-sharing app. Our app will include the following features:

    • CRUD (Create, Read, Update, Delete) database functionality
    • a user management system, so that users will be able to create an account, upload photos, see other people’s photos and edit or delete their own photos
    • a simple web interface made with Bootstrap

    Note: although this app seems quite similar to a social network, it isn’t one. An app like Instagram or Twitter has a lot of complexity that can’t be covered in a single article.

    Tech stack

    Let’s define the technologies we’re going to use. We’ll cover the installation process of each one when we need to use it.

    On the back end, Django will be the core framework of the app. It allows us to define the URLs, define the logic, manage user authentication, and control all the database operations through the Django ORM (object-relational mapper).

    Also, we’ll be using a couple of third-party packages to accelerate the development of some features.

    Django-taggit provides us the ability to set up a simple tag system in few steps. Pillow is a Python package that provides Django image manipulation capabilities. Finally, Django-crispy-forms gives us a simple way to display Bootstrap forms.

    On the front end, we’re going to use the Django template language, which consists of HTML files that display data dynamically.

    We’ll also be using Bootstrap 5 (the latest version at the time of writing) for the design of the site.

    Note: you can always check the dependencies used in this project in the requirements.txt file.

    Create a Django project

    Let’s start with Django!

    First of all, make sure you have Python 3 installed. Most Linux and macOS systems have already Python installed, but if you use Windows you can check the Python 3 installation guide.

    Note: we’ll be using Unix commands (macOS & Linux) along the tutorial. If you can’t execute them for any reason you can use a graphical file manager.

    In some linux distributions, the python command refers to Python 2. In others, python doesn’t exist at all.

    Let’s see what Python command you need to use to follow along. Open your terminal (on Unix) or command line window (on Windows) and type python --version:

    python --version
    # My result
    Python 3.9.5

    If you’ve got a Python version above 3.6, you’re ready to go. If you don’t have the right version of Python, you might get a message like one of these:

    Command 'python' not found
    Python 2.7.18

    The Python command you need to run to follow along with this tutorial will be python3:

    python3 --version
    Python 3.9.5

    Virtual environments

    A virtual environment is an isolated Python environment, which includes all the files you need to run a Python program.

    Virtual environments are a crucial part of any Python (and Django) project, because they let us manage and share dependencies (external packages the project depends on) with other people.

    To create a virtual environment natively, we’ll use the built-in module venv, available from Python 3.6 or greater.

    The following command will create a virtual environment with the name .venv (you can choose another name if you prefer):

    python -m venv .venv

    If you’re using Ubuntu Linux, or any other Debian-based distribution, it’s possible you’ll get the following message:

    The virtual environment was not created successfully because pip is not available ... 

    To solve this, you can run the following command:

    sudo apt-get install python3-venv

    If the command above doesn’t work, you can use virtualenv, which is another library to work with virtual environments:

    virtualenv .venv

    After running this command, a folder named .venv (or the name you’ve chosen) will appear.

    All of the packages we install will be placed inside that directory.

    To activate a virtual environment, you’ll need to run a specific command depending on your OS. You can refer to the table below (extracted from the Python docs).

    Platform Shell Command to activate virtual environment
    POSIX bash/zsh $ source .venv/bin/activate
    fish $ source .venv/bin/
    csh/tcsh $ source .venv/bin/activate.csh
    PowerShell Core $ .venv/bin/Activate.ps1
    Windows cmd.exe C:> .venv\Scripts\activate.bat
    PowerShell PS C:> .venv\Scripts\Activate.ps1

    Since I’m using a bash shell on a POSIX operative system, I’ll use this:

    source .venv/bin/activate

    Note how a .venv caption is added to my shell once I’ve activated the virtualenv.

    Virtual environment activated

    Installing Django

    Django is an external package, so we’ll need to install it with pip:

    pip install django
    # Use pip3 if the command above doesn't work
    pip3 install django

    Note: we can always take a look at the packages installed in our venv with pip freeze.

    Next, let’s start a Django project with the name config with the command-line utility django-admin.

    django-admin startproject config

    Here, config is the name of the project, and it’s used as a naming convention to keep all your projects with the same structure. For instance, Django cookiecutter uses this convention name to start a project.

    That being said, you can create the project with any other name.

    After running these commands, you should have the regular file structure of a Django project. You can check it with the command-line utility tree, or with any file manager.

    Note: if you can’t run tree you’ll need to install it.

    $ tree config/
    └── config
        ├── config
        │   ├──
        │   ├──
        │   ├──
        │   ├──
        │   └──

    Now let’s enter the project folder with cd, and run the server to check everything is correctly set up:

    cd config/
    python runserver

    You’ll see a warning message pointing out that there are unapplied migrations. This is a totally normal message, and we’ll learn how to run migrations in the “Creating the Photo Model” section.

    Now, visit localhost:8000 in your browser. You should see the iconic Django congratulations page.

    Django congrats page

    Starting the Photo-sharing App

    The file has the exact same capabilities as django-admin, so we’ll use it many times during this tutorial.

    Its location is in the root folder of the project, and each time we want to run a command with it, we need to enter the project directory.

    Remember to always list the files of the directory you’re in with ls, to check if we’re in the correct spot:

    $ ls

    With these tips in mind, it’s time to start the main app of the project. To do this we open a new shell (so the local server is still running), and use the with the command startapp.

    Note: each time we open a new shell session, we’ll need to activate the virtual environment again.

    source .venv/bin/activate
    cd config
    python startapp photoapp

    In this case, the name of the app is photoapp. Once again, you can create it with whatever name you want.

    Every time we create an app we must install it. We can do this in the config/ file by adding photoapp to the INSTALLED_APPS variable:

    # config/
        # Custom apps

    Next, we’ll enter the app directory and create an empty file. We can do this by running touch, or by creating it with a graphical file manager:

    cd photoapp/

    Lastly, let’s include all the URL patterns of the photo-sharing app in the overall project. To accomplish this, we’ll use the django.urls.include function:

    # config/
    from django.urls import path, include # Import this function
    urlpatterns = [
        # Main app
        path('', include('photoapp.urls')),

    The code above will include all the URL patterns of the photoapp/ to the project.

    If you take a look at the shell in which the server is running, you’ll see an error:

    raise ImproperlyConfigured(msg.format(name=self.urlconf_name)) ....

    That’s because we haven’t created the urlpatterns list inside the photopp/ file.

    To solve this, create an empty list named urlpatterns. We’re going to populate that variable later with Django paths:

    # photoapp/
    # Empty patterns
    urlpatterns = [

    Note: the advantage of using this approach is that we can make the photoapp reusable, by including all the code needed inside of it.

    Creating the Photo Model

    In this section, we’re going to build the database schema of our application. For this purpose, we’ll use the Django ORM.

    The Django ORM allows the creation and management of database tables without the need to use SQL manually.

    When we write a model, it represents a database table, and each attribute inside it represents a column.

    Since we’ll use the Django built-in authentication system, we can start focusing on the app’s core functionality. That way, we avoid building a custom user management system.

    Before starting, we’re going to install some third-party packages, django-taggit and Pillow. We can do so with the following command:

    pip install django-taggit Pillow

    django-taggit is a Django application, so we need to install it as we did with the photoapp:

    # config/
        # 3rd party apps
        # Custom apps
    # Django taggit

    The TAGGIT_CASE_INSENSITIVE variable configures the tags to be case insensitive. That means PYTHON and python will be the same.

    Let’s define the Photo model, which will be the main model of the app. Open the photoapp/ file and use the following code:

    # photoapp/
    from django.db import models
    from django.contrib.auth import get_user_model
    from taggit.managers import TaggableManager
    class Photo(models.Model):
        title = models.CharField(max_length=45)
        description = models.CharField(max_length=250) 
        created = models.DateTimeField(auto_now_add=True)
        image = models.ImageField(upload_to='photos/')
        submitter = models.ForeignKey(get_user_model(), on_delete=models.CASCADE)
        tags = TaggableManager() 
        def __str__(self):
            return self.title

    In the above code block, we’ve defined the Photo model. Let’s see what each field does.

    • The title field is a CharField and it’s limited to 45 characters.

    • description is another CharField but with a limit of 250 characters.

    • created is a DateTimeField and, as the name suggests, it stores the date and hour when the photo is created.

    • image is an ImageField. It uploads the images to media/photos and stores the URL at which the file is located. Later we’ll see how to set up media files.

    • submitter is a ForeignKey, which means it’s a relationship with a user and the photo uploaded. That way we can filter which user uploaded a photo.

    • Lastly, tags is a TaggableManager and allows us to classify topics by tags.

    On the other hand, the __str__ method indicates how each object will be displayed in the admin area. Later, we’ll set up the admin and create our firsts objects.

    To create a database based on the model we created, we firstly need to make the migrations and then run them.

    Enter the project root directory and use the script with the following arguments:

    python makemigrations
    python migrate

    The makemigrations command will create a migrations file based on the Photo model.

    Note: the Migrations are Python scripts that produce changes in the database based on the models.

    We can see exactly what’s happening with that migration by opening the photoapp/migrations/ file:

    # photoapp/migrations/
    # imports ...
    class Migration(migrations.Migration):
        initial = True
        dependencies = [
            ('taggit', '0003_taggeditem_add_unique_index'),
        operations = [
                    ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),

    Tip: never modify migrations file by hand. All the migrations must be auto-generated by Django.

    The migrate command creates database tables by running all the migrations.

    After running these two commands, you should see an SQLite database in the project root folder. If we inspect it with DB Browser, we’ll see all the fields related to the Photo model.

    SQlite Visualizer

    Managing Media Files in Development

    The photo-sharing app depends heavily on media files. It’s all about sharing images, it isn’t?

    Media files in Django are all the files uploaded by the user. For now, we’re going to set up media files in development, since we’ll only interact with the app through the local server.

    To enable media files in development, we create the MEDIA_URL and MEDIA_ROOT variables inside the settings file. Also, we need to modify the urlpatterns of the overall project to serve media files from the local server.

    First, we need to edit the config/ file and append the following code at the end of the file:

    # config/
    # Other settings ...
    MEDIA_URL = '/media/'
    MEDIA_ROOT = BASE_DIR / 'media/'

    MEDIA_URL is the URL that handles all the media uploaded to the MEDIA_ROOT folder. In this case, the absolute media URL would look like this: http://localhost:8000/media/.

    On the other hand, MEDIA_ROOT is the path that points to the folder where all the media will be placed.

    Remember that, since we’re using the pathlib library, we’re able to concatenate paths with /.

    We can think of MEDIA_ROOT as the physical storage where the images will be uploaded, and MEDIA_URL as the URL that points to that storage.

    If we want Django to manage media files, we’ll need to modify the project URLs:

    # config/
    # New imports
    from django.conf import settings
    from django.conf.urls.static import static
    urlpatterns = [
        # Main app
        path('', include('photoapp.urls')),
    ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

    Taking this into account, the absolute URL of the uploaded photos will be: http://localhost:8000/media/photos/. This because we set the upload_to attribute as photos/.

    Note: it can be dangerous to accept uploaded files from the user. Check out this list of security considerations.

    When working with an app that’s publicly available, we must be careful with media files. We could suffer DoS attacks. Users could also upload malicious content, so the recommended approach is to always use a CDN to solve this kind of problem.

    For now, you can forget about security problems, since we’re working with a development project and the ImageField only accepts a predeterminate set of extensions.

    You can check those valid extensions by running the following code in the Django shell (making sure your venv is activated):

    $ python shell
    >>> from django.core.validators import get_available_image_extensions
    >>> get_available_image_extensions()
    ['blp', 'bmp', 'dib', 'bufr', 'cur', 'pcx', 'dcx', 'dds', 'ps', 'eps', 'fit', 'fits', 'fli', 'flc', 'ftc', 'ftu', 'gbr', 'gif', 'grib', 'h5', 'hdf', 'png', 'apng', 'jp2', 'j2k', 'jpc', 'jpf', 'jpx', 'j2c', 'icns', 'ico', 'im', 'iim', 'tif', 'tiff', 'jfif', 'jpe', 'jpg', 'jpeg', 'mpg', 'mpeg', 'mpo', 'msp', 'palm', 'pcd', 'pdf', 'pxr', 'pbm', 'pgm', 'ppm', 'pnm', 'psd', 'bw', 'rgb', 'rgba', 'sgi', 'ras', 'tga', 'icb', 'vda', 'vst', 'webp', 'wmf', 'emf', 'xbm', 'xpm']

    Testing Models with Django Admin

    Django admin is a built-in interface where administrative users can make CRUD operations with the registered models of the project.

    Now that we’ve created the photo model and set up the media files, it’s time to create our first Photo object through the admin page.

    To do this, we have to register the Photo model into the admin page. Let’s open the photoapp/, import the Photo model, and pass it as a parameter to the function:

    # photoapp/
    from django.contrib import admin
    from .models import Photo # We import the photo model
    # Register your models here.

    Next, it’s time to create a superuser to be able to access the admin page. We can do this with the following command:

    python createsuperuser
    Username: daniel 
    Email address: 
    Password (again): 
    Superuser created successfully

    You can leave the superuser without email for now, since we’re using the default auth user.

    After creating the superuser, jump into the browser and navigate to http://localhost:8000/admin.

    It’ll redirect you to the login page, where you’ll need to fill in your credentials (those you created the user with).

    Django admin login page

    After entering our credentials, we’ll have access to a simple dashboard, where we can start to create photos. Just click the Photos section and then the Add button.

    Django dashboard

    Here’s what filling the creation fields looks like.

    Filling out content

    Uploading an image can be done simply with drag-and-drop.

    Uploading images

    After hitting the Save button, we’ll see a dashboard with all the created photos.

    Photo dashboard

    Handling Web Responses with Views

    We’ve defined the database schema of a working app, and even created some objects with the Django admin. But we haven’t touched the most important part of any web app — the interaction with the user!

    In this section, we’re going to build the views of the photo-sharing app.

    Broadly speaking, a view is a Python callable (Class or function) that takes a request and returns a response.

    According to the Django documentation, we should place all of our views in a file named inside each app. This file has already been created when we started the app.

    We have two main ways to create views: using function-based views (FBVs) or class-based views (CBVs).

    CBVs are the best way to reuse code — by applying the power of Python class inheritance into our views.

    In our application, we’ll be using generic views, which allow us to create simple CRUD operations by inheriting Django pre-built classes.

    Before starting, we’ll import all the stuff we need to build the views. Open the photoapp/ file and paste the code below:

    # photoapp/
    from django.shortcuts import get_object_or_404
    from django.core.exceptions import PermissionDenied
    from django.views.generic import ListView, DetailView, CreateView, UpdateView, DeleteView
    from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
    from django.urls import reverse_lazy
    from .models import Photo

    Let’s see what we’re importing here:

    • get_object_or_404 is a shortcut that allows us to retrieve an object from the database, preventing a DoesNotExists error and raising a HTTP 404 exception.

    • PermissionDenied raise an HTTP 403 exception when called.

    • The pre-built generic views help us to build CRUD functionality with few lines of code.

    • We’ll use the LoginRequiredMixin and UserPassesTestMixin to assert the users have the right permissions when accessing to a view.

    • reverse_lazy is used in CBVs to redirect the users to a specific URL.

    • We need to import Photo in order to retrieve and update database rows (photo objects).

    Note: you can access the file on GitHub.

    Photo Lists Views

    The generic List View will help us to display many objects of a Model. We’ll compare it with the DetailView later.

    In this section, we’re going to build two main Views. The PhotoListView passes as context all the photos uploaded by any user, and the PhotoTagListView takes a tag slug as the argument to show up the photos.

    The code below defines the PhotoListView inheriting from ListView:

    # photoapp/
    class PhotoListView(ListView):
        model = Photo     
        template_name = 'photoapp/list.html'
        context_object_name = 'photos'

    First, we inherit the ListView and therefore receive all the behavior from that class.

    Remember, you can always check the source code of any Django class in the official GitHub repo.

    Then we define the model we’re reading the data from, the template we’re going to use (we’ll build the front end later), and the name of the context object we can use to access the data in the template.

    Now, it’s time to declare the PhotoTagListView. This view is a little bit more complex, since we have to play with the get_queryset() and get_context_data() methods:

    # photoapp/
    class PhotoListView(ListView): ...
    class PhotoTagListView(PhotoListView):
        template_name = 'photoapp/taglist.html'
        # Custom method
        def get_tag(self):
            return self.kwargs.get('tag')
        def get_queryset(self):
            return self.model.objects.filter(tags__slug=self.get_tag())
        def get_context_data(self, **kwargs):
            context = super().get_context_data(**kwargs)
            context["tag"] = self.get_tag()
            return context

    Here, we’re inheriting all the attributes of the PhotoListView. That means we’re using the same model and context_object_name, but we’re changing the template_name.

    This view may seem the same as the previous one, except that we’re dealing with custom methods.

    We’re creating a custom method get_tag to receive the tag slug from the response Django is going to take and return it. We do it this way because we’re going to use that function in two places.

    The get_queryset method is set to return self.model.objects.all() by default. We’ve modified it to return only the photo objects tagged with the slug passed to the URL.

    Finally, the get_context_data was modified to also return the tag passed to the URL. This is because we’ll display it later in a template.

    Photo Detail View

    This view is a simple DetailView that displays all the data related to a unique photo. This includes the title, description, and tags of the desired photo:

    # photoapp/
    class PhotoListView(ListView): ...
    class PhotoTagListView(PhotoListView): ...
    class PhotoDetailView(DetailView):
        model = Photo
        template_name = 'photoapp/detail.html'
        context_object_name = 'photo'

    We do pretty much the same process as we did with the list views. The only difference is that we’re returning a single object instead of many, and using a different template.

    Create photo view

    This view allows users to create a photo object only if they’re logged in. We don’t want anonymous users to be able to upload content to our platform. That would be scary!

    The simplest way to protect this functionality with Django is to create a class that inherits from CreateView and LoginRequiredMixin. The LoginRequiredMixin tests if a user is logged in. If the user isn’t logged in, they’re redirected to the login page (which we’ll build later):

    # photoapp/
    class PhotoListView(ListView): ...
    class PhotoTagListView(PhotoListView): ...
    class PhotoDetailView(DetailView): ...
    class PhotoCreateView(LoginRequiredMixin, CreateView):
        model = Photo
        fields = ['title', 'description', 'image', 'tags']
        template_name = 'photoapp/create.html'
        success_url = reverse_lazy('photo:list')
        def form_valid(self, form):
            form.instance.submitter = self.request.user
            return super().form_valid(form)

    In this view, Django will create a form with the title, description, image and tags fields.

    We’re also using the sucess_url attribute. Users will be redirected to the photo dashboard if the photo creation was successful.

    If we take a closer look at the form_valid method, we’ll notice that it’s setting up the user that’s making the request as the submitter of the photo form.

    Update and delete photo views

    We want the users to be able to modify or delete a photo only if they’re the submitters.

    Handling conditional authentication can be difficult if we’re using CBVs. However, we can make use of TestMixins to accomplish this task.

    Let’s create a test mixin UserIsSubmitter that checks if the user that’s trying to update or delete a photo actually submitted it:

    # photoapp/
    class PhotoListView(ListView): ...
    class PhotoTagListView(PhotoListView): ...
    class PhotoDetailView(DetailView): ...
    class PhotoCreateView(LoginRequiredMixin, CreateView): ...
    class UserIsSubmitter(UserPassesTestMixin):
        # Custom method
        def get_photo(self):
            return get_object_or_404(Photo, pk=self.kwargs.get('pk'))
        def test_func(self):
            if self.request.user.is_authenticated:
                return self.request.user == self.get_photo().submitter
                raise PermissionDenied('Sorry you are not allowed here')

    First, we’ve created a custom method get_photo that returns a Photo object, with the primary key specified in the URL. If the photo doesn’t exist, it raises an HTTP 404 error.

    Then we’ve defined the test function. It will only return true if the user is logged in and is the photo submitter.

    If the user isn’t logged in, it’ll raise a PermissionDenied exception.

    On the other hand, the PhotoUpdateView and PhotoDeleteView are children of the mixin we created, but also UpdateView and DeleteView respectively:

    # photoapp/
    class PhotoListView(ListView): ...
    class PhotoTagListView(PhotoListView): ...
    class PhotoDetailView(DetailView): ...
    class PhotoCreateView(LoginRequiredMixin, CreateView): ...
    class UserIsSubmitter(UserPassesTestMixin): ...
    class PhotoUpdateView(UserIsSubmitter, UpdateView):
        template_name = 'photoapp/update.html'
        model = Photo
        fields = ['title', 'description', 'tags']
        success_url = reverse_lazy('photo:list')
    class PhotoDeleteView(UserIsSubmitter, DeleteView):
        template_name = 'photoapp/delete.html'
        model = Photo
        success_url = reverse_lazy('photo:list')         

    The PhotoUpdateView inherits the test function from the UserIsSubmitter mixin and the update functionality from the UpdateView.

    The fields attribute defines the fields the user will be able to edit. We don’t want the image to be changed, and neither the creation date or the submitter.

    On the other hand, the PhotoDeleteView also inherits the test function but deletes the photo instead of updating it.

    Both views redirect the user to the list URL if everything went well.

    That’s all for the views. Now, let’s create a simple authentication app and complete the project.

    URL Patterns

    We’re almost there. We’ve already defined the database schema and how the user will create and update photos. Let’s see how to handle the URL configuration the photo-sharing app.

    Do you remember when we created an empty urlpatterns variable at the start of the project? It’s time to populate it!

    First, let’s import all the views and functions we need:

    # photoapp/
    from django.urls import path
    from .views import (

    The path function receives two arguments, route and view, and an optional argument, name, which is used as part of the namespace:

    # photoapp/
    app_name = 'photo'
    urlpatterns = [
        path('', PhotoListView.as_view(), name='list'),
        path('tag/<slug:tag>/', PhotoTagListView.as_view(), name='tag'),
        path('photo/<int:pk>/', PhotoDetailView.as_view(), name='detail'),
        path('photo/create/', PhotoCreateView.as_view(), name='create'),
        path('photo/<int:pk>/update/', PhotoUpdateView.as_view(), name='update'),
        path('photo/<int:pk>/delete/', PhotoDeleteView.as_view(), name='delete'),

    Explaining this configuration, the app_name variable declares the namespace of the app.

    That means that whether we’re using the reverse function in views, or the {% url %} tag in templates, we’ll need to use the following namespace:


    If you want to know more about how the Django URL dispatcher works, feel free to read the documentation.

    Authentication System

    In this project, we’re going to use the default Django authentication system.

    This is because our main focus is to have a functional application as soon as possible. However, we’ll create a custom app, because we want to add sign-up functionality to the project.

    At first, we create a users app and do all the same installation process as we did with the photoapp:

    python startapp users
    # config/
        # 3rd party apps
        # Custom apps

    Next, we create the file as we did with the photo app:

    cd users/

    Then we include the user’s URLs in the overall project:

    # config/
    urlpatterns = [
        # Main app
        path('', include('photoapp.urls')),
        # Auth app
        path('users/', include('users.urls')),
    ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

    Then we write a SignUpView to allow the user to register through the site:

    # users/
    from django.views.generic import CreateView
    from django.contrib.auth import authenticate, login
    from django.contrib.auth.forms import UserCreationForm
    from django.urls import reverse_lazy
    class SignUpView(CreateView):
        template_name = 'users/signup.html'
        form_class = UserCreationForm
        success_url = reverse_lazy('photo:list')
        def form_valid(self, form):
            to_return = super().form_valid(form)
            user = authenticate(
            login(self.request, user)
            return to_return

    This view is a CreateView and works with the built-in UserCreationForm to create a new user.

    We’re using the form_valid method to log in the users before redirecting them to the photo dashboard.

    We’ll create a login view because we want to use a custom template to display the login page. To do this, we’ll import the built-in LoginView and inherit from it:

    # Previous imports
    from django.contrib.auth.views import LoginView
    class SignUpView(CreateView): ...
    class CustomLoginView(LoginView):
        template_name = 'users/login.html'

    Finally, it’s time to create the URL routing:

    # users/
    from django.urls import path
    from django.contrib.auth.views import LogoutView
    from .views import SignUpView, CustomLoginView
    app_name = 'user'
    urlpatterns = [
        path('signup/', SignUpView.as_view(), name='signup'),
        path('login/', CustomLoginView.as_view(), name='login'),
        path('logout/', LogoutView.as_view(), name='logout'),

    Once again, we’re using the app_name variable. So the namespace of the user application would be like this:


    We’re setting up three URLs. The signup/ and login/ are using the custom views we created, but the logout/ URL is using the Django built-in LogoutView.

    Before continuing, let’s configure the authentication redirects in the config/ file:

    # Other settings ...
    USE_TZ = True
    # Django Authentication
    LOGIN_URL = 'user:login'
    LOGIN_REDIRECT_URL = 'photo:list'
    LOGOUT_REDIRECT_URL = 'photo:list'

    This tells Django that the login URL is the custom user login URL, and that when the users are logged in they must be redirected to the photo dashboard.

    The Front End

    After building the back end (what the user can’t see) with Django, it’s time to build the front end (what the user does see).

    For that purpose, we’re going to use the Django template language and Bootstrap 5. This allows us to generate HTML dynamically and to produce a different output depending on the state of our database. We can save a lot of code by working with template inheritance. Using Bootstrap 5 means we won’t be using static files.

    Writing the base template

    In this section, we’re going to build the base.html file, which is the template all the others will inherit from.

    To do this we must change the DIRS key inside the TEMPLATES variable located in the settings file:

    # config/
            # Options ..
            'DIRS': [BASE_DIR / 'templates'],
            'APP_DIRS': True,
            # More options

    The default behavior of Django is to search for template files inside the templates/ folder of each app.

    For example, the templates of the photo-sharing app can be found in photoapp/templates. It’s the same story for the users app (users/templates).

    By assigning the DIRS key to [BASE_DIR / 'templates'], we’re telling Django to also search for templates inside of a folder named templates.

    Create a directory templates at the root of the project (where the file is located) and touch the base.html and navbar.html templates:

    mkdir templates && cd templates
    touch base.html navbar.html

    Concluding the templates of our project can be found in any of these three directories:

    ├── photoapp
    │   └── templates
    │       └── photoapp
    ├── templates
    └── users
        └── templates
            └── users

    Remember that you can always check the project structure on the GitHub repository.

    Inside the base.html template, we’re going to set up the basic HTML structure, some meta tags, links to the bootstrap CDN, and blocks that other templates will use:

    <!-- templates/base.html -->
    <!DOCTYPE html>
    <html lang="en">
        <meta charset="UTF-8" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Django Photo Sharing app</title>
        {% include 'navbar.html' %}
        <div class="container mt-4">
        {% block body %} 
        {% endblock body %}

    The {% include %} tag (as the name suggests) includes all the code of the selected template inside base.html file.

    Therefore, all the code present inside the navbar.html will be placed at the start of the body.

    Note: there’s a lot of HTML and Bootstrap here. Feel free to copy it all, since it’s not the main focus of the tutorial.

    Below is the HTML template code for the navbar. This navbar will contain some logic to show up a link to the login page, in case the user isn’t logged in:

    <!-- templates/navbar.html -->
    <nav class="navbar navbar-expand-md navbar-dark bg-dark">
      <div class="container-fluid">
        <a class="navbar-brand" href="{% url 'photo:list' %}">Photo App</a>
          aria-label="Toggle navigation"
          <span class="navbar-toggler-icon"></span>
          class="collapse navbar-collapse flex-row-reverse"
          <ul class="navbar-nav">
            {% if user.is_authenticated %}
            <li class="nav-item">
              <a class="nav-link active" href="{% url 'photo:create' %}">Add a photo</a>
            <li class="nav-item">
              <a class="nav-link active" href="#">Hi {{user.username}}</a>
            {% else %}
            <li class="nav-item">
              <a href="{% url 'user:login' %}" class="btn btn-sm btn-danger"
                >Sign In</a
            {% endif %}

    Here’s how the template will be shown when the user is logged in.

    Navbar when the user is logged in

    Below is what’s presented when the user isn’t logged in.

    Navbar when the user isn't logged in

    Don’t worry if you get an error in your browser. We haven’t built the photo sharing templates yet.

    Photo-sharing Templates

    We’re going to write all the files needed in the photo-sharing app. That includes the templates used to accomplish the CRUD operations.

    All of these templates will extend the base.html template and will be located in the photoapp/templates/photoapp directory.

    But before working with forms in templates we’ll use Django crispy forms to stylize our app:

    pip install django-crispy-forms

    Once again, crispy_forms is a Django app, and we need to include it on the INSTALLED_APPS list:

    # config/
        # 3rd party apps
        # Custom apps
    # Indicates the frontend framework django crispy forms will use
    CRISPY_TEMPLATE_PACK = 'bootstrap4'

    We use the template pack of Bootstrap 4, because the Bootstrap form classes are compatible between the 4th and 5th version (at the time of writing).

    You may remember we used the following template names on the photoapp/


    That means all of these templates will be located in photoapp/templates/photoapp.

    To create this folder, go to the photo-sharing app and create a directory templates/, and inside it create another folder named photoapp/:

    cd photoapp/
    mkdir -p templates/photoapp/
    cd templates/photoapp/

    Now create all the templates we declared on the views:

    touch list.html taglist.html detail.html create.html update.html delete.html

    List templates

    The list.html will inherit from the base.html template, and therefore all the HTML structure will appear in the source code:

    <!-- photoapp/templates/photoapp/list.html -->
    {% extends 'base.html' %} 
    {% block body %}
    <div class="row">
      {% for photo in photos %}
      <div class="col-lg-3 col-md-4 col-xs-6">
        <a href="{% url 'photo:detail' %}" class="d-block mb-4 h-100">
          <img src="{{photo.image.url}}" class="img-fluid rounded" alt="{{photo.title}}" />
      {% endfor %}
    {% endblock body %}

    We’re using the template tag for loop, which iterates over the photos and displays them with Bootstrap rows and columns.

    Don’t forget to create multiple photo objects in the Django admin.

    Visit localhost:8000/ to see how the template looks.

    List template

    The taglist.html template will inherit from the list.html we just created:

    <!-- photoapp/templates/photoapp/taglist.html -->
    {% extends 'photoapp/list.html' %}
    {% block body %}
    <div class="alert alert-primary">
        <h2 class="text-center">Photos with the tag {{tag}}</h2>
    {{ block.super }}
    {% endblock body %}

    We’re just modifying a bit this template. That’s why we’re calling {{ block.super }}, which contains all the code inside the body block of the list.html template.

    Create a couple of objects with the tag code before continuing.

    Go to localhost:8000/tag/code/, where the code is the slug of the tag.

    List template tag

    Remember that the taglist URL has the following form:


    Here, <slug:tag> refers the name of the tag.

    Detail photo template

    Let’s edit the detail.html template to be able to see our photos in detail:

    <!-- photoapp/templates/photoapp/detail.html -->
    {% extends 'base.html' %} 
    {% block body %}
    <div class="mx-auto">
      <h1 class="text-center">{{ photo.title }}</h1>
      <p class="text-center fw-light">Uploaded on: {{photo.created}} <br> By {{photo.submitter.username}}</p>
      {% if user == photo.submitter %}
        <p class="text-center">
          <span><a href="{% url 'photo:update' %}" class="text-primary px-2">Update</a></span>
          <span><a href="{% url 'photo:delete' %}" class="text-danger px-2">Delete</a></span>
      {% endif %}
    <div class="row pb-5">
      <div class="col-md-8">
        <img src="{{photo.image.url}}" alt="" width="100%" />
      <div class="col-md-4">
        <h4>More about this photo:</h4>
        <ul class="list-group list-group-horizontal-lg list-unstyled py-4">
          {% for tag in photo.tags.all %}
            <li><a href="{% url 'photo:tag' tag.slug %}" class="btn btn-sm list-group-item list-group-item-primary">{{}}</a></li>
          {% endfor %}
        <p>{{ photo.description }}</p>
    {% endblock body %}

    Let’s see how the template looks before digging into the functionality. Follow localhost:8000/photo/1.

    Photo in Detail

    Here, we’re accessing the photo properties from the templates through the dot notation. That’s because photo.submitter.username is equal to daniel.

    We implement a little bit of logic to show up the links to update or delete the photo in case the user is also the submitter.

    Finally, we show up all the tags of the photo iterating over photo.tags.all.

    Create the photo template

    The next template will include a crispy form, so that we don’t have to display the forms manually. Django will do that for us:

    <!-- photoapp/templates/photoapp/create.html -->
    {% extends 'base.html' %}
    {% load crispy_forms_tags %}
    {% block body %}
    <div class="mx-auto">
      <h1 class="mt-3 text-center">Add photo</h1>
    <div class="form-group">
      <form action="" method="post" enctype="multipart/form-data">
        {% csrf_token %}
        {{ form|crispy }}
        <button type="submit" class="btn btn-success mb-3">Add Photo</button>
    {% endblock body %}

    Each time we use crispy forms, we need to load the tags with {% load crispy_forms_tags %}.

    It’s extremely important to include enctype="multipart/form-data", because if we don’t the files won’t be uploaded. Here’s a really good response to the implications of using it in forms.

    Every Django form must include a {% csrf_token %} inside. You can learn more about this tag on the “Cross Site Request Forgery protection” page.

    Notice how we simply display the form with {{form|crispy}}. If you know what pipes are in Linux, we’re doing exactly that by redirecting the form provided by the view to the crispy filter.

    Go to the add photo URL to check if the photo is uploaded.

    Uploading a photo

    If everything went well, we should see the added photo in the dashboard.

    Added photo

    Update and delete templates

    Let’s finish the photo-sharing app before heading to the authentication templates.

    The following update template is a simple form where the user can update the title, description, and tags of the photo:

    <!-- photoapp/templates/photoapp/update.html -->
    {% extends 'base.html' %}
    {% load crispy_forms_tags %}
    {% block body %}
    <div class="mx-auto">
      <h1 class="mt-3 text-center">Edit photo {{photo}}</h1>
    <div class="form-group">
      <form action="" method="post" enctype="multipart/form-data">
        {% csrf_token %}
        {{ form|crispy }}
        <button type="submit" class="btn btn-success mb-3">Edit Photo</button>
    {% endblock body %}

    We can take see how it looks at localhost:8000/photo/1/update.

    Updating a Photo

    We also want to give users the option to delete a photo. With the following template, they can decide to delete the photo or not:

    <!-- photoapp/templates/photoapp/delete.html -->
    {% extends 'base.html' %} 
    {% block body %}
    <div class="form-group mx-auto">
      <h2 class="text-center">
        You are going to <span class="text-danger">delete</span>: "<i
          >{{ photo }}</i
      <p class="text-center">Are you sure, you want to delete the photo ?</p>
      <div class="form-group">
          class="d-flex flex-column align-items-center justify-content-center"
          {% csrf_token %}
          <div class="row">
            <div class="col">
              <a href="{% url 'photo:detail' %}" class="btn btn-primary"
            <div class="col">
              <button type="submit" class="btn btn-danger">Delete</button>
          <p>This action is irreversible</p>
    {% endblock body %}

    The deletion page would look like this.

    Delete template

    If the user decides to cancel, they’re redirected to the detail page of that photo.

    User Authentication Templates

    The purpose of this section is to write all the templates related to the authentication. We’ll write the signup.html and login.html templates.

    Similar to the photo-sharing app, all of the following templates will be located in a double folder structure: users/templates/users/.

    Enter the users app and create the folders in which the templates will be located:

    # Enter to the project root directory
    cd ../../../
    cd users/
    mkdir -p templates/users/

    Create the sign-up and login template files inside that folder:

    cd templates/users/
    touch signup.html login.html

    Below is the template code for the signup.html template:

    <!-- users/templates/users/signup.html -->
    {% extends 'base.html' %} 
    {% load crispy_forms_tags %}
    {% block body %}
    <div class="mx-auto">
      <div class="form-group">
        <form action="" method="post">
          {% csrf_token %} 
          {{ form|crispy }}
          <button type="submit" class="btn btn-danger w-100 my-3">Create account</button>
      {% comment %}  Already Registered {% endcomment %}
      <div class="text-center w-100">
        <p class="text-muted font-weight-bold">
          Already Registered?
          <a href="{% url 'user:login' %}" class="text-primary ml-2"> Login </a>
    {% endblock body %}

    We can check it out in the browser at localhost:8000/users/signup.

    SignUp page

    Last but not least, write the login template:

    <!-- users/templates/users/login.html -->
    {% extends 'base.html' %} 
    {% load crispy_forms_tags %}
    {% block body %}
    <div class="mx-auto">
      <div class="form-group">
        <form action="" method="post">
          {% csrf_token %} 
          {{ form|crispy }}
          <button type="submit" class="btn btn-danger w-100 my-3">Sign in</button>
      {% comment %}  Already Registered {% endcomment %}
      <div class="text-center w-100">
        <p class="text-muted font-weight-bold">
          Don't have an account?
          <a href="{% url 'user:signup' %}" class="text-primary ml-2">Create account</a>
    {% endblock body %}

    Login Page

    Django templates allow us to save a lot of time by reusing the same HTML multiple times. Just image how much time you’d expend by copy and pasting the same HTML over and over.

    Perfect! Now you have a completely working application. Try to use it, modify it, or even expand its functionality.

    Summing Up

    Congratulations! You’ve created a full-stack project from scratch.

    Django is the most-used Python web framework. It allows you to quickly build complex web applications.

    It has a lot of built-in features that accelerate the development process, like server-side template rendering, Class-based views, and Model forms.

    Django also offers several third-party packages that give you the option to use someone else’s app. As an example, the project works with Django taggit and Django crispy forms.

    In this tutorial, we covered the following:

    • Django CRUD operations
    • the Django Built-in authentication system
    • how to manage media files in Django
    • using Django taggit to classify content
    • implementing Django forms with crispy forms
    • writing Django templates with Bootstrap 5

    The best way to keep learning and advancing it to apply the knowledge you’ve acquired to new and challenging projects. Good luck!