Build a Photo-sharing App with Django – SitePoint

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

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/activate.fish

csh/tcsh
$ source .venv/bin/activate.csh

PowerShell Core
$ .venv/bin/Activate.ps1
Windows
cmd.exe
C:> .venvScriptsactivate.bat

PowerShell
PS C:> .venvScriptsActivate.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.

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

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
│ ├── asgi.py
│ ├── __init__.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
└── manage.py

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

python manage.py 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.

Starting the Photo-sharing App
The manage.py 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
Another-files.. manage.py

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 manage.py 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 manage.py 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/settings.py file by adding photoapp to the INSTALLED_APPS variable:

INSTALLED_APPS = [
‘django.contrib.admin’,

‘photoapp’,
]

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

touch urls.py

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:

from django.urls import path, include

urlpatterns = [
path(‘admin/’, admin.site.urls),

path(”, include(‘photoapp.urls’)),
]

The code above will include all the URL patterns of the photoapp/urls.py 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/urls.py file.
To solve this, create an empty list named urlpatterns. We’re going to populate that variable later with Django paths:

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:

INSTALLED_APPS = [

‘taggit’,

‘photoapp’,
]

TAGGIT_CASE_INSENSITIVE = True

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/models.py file and use the following code:

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 manage.py script with the following arguments:
python manage.py makemigrations

python manage.py 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/0001_initial.py file:

class Migration(migrations.Migration):

initial = True

dependencies = [
(‘taggit’, ‘0003_taggeditem_add_unique_index’),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]

operations = [
migrations.CreateModel(
name=’Photo’,
fields=[
(‘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.

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/settings.py file and append the following code at the end of the file:

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:

from django.conf import settings
from django.conf.urls.static import static

urlpatterns = [
path(‘admin/’, admin.site.urls),

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 manage.py 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/admin.py, import the Photo model, and pass it as a parameter to the admin.site.register function:

from django.contrib import admin
from .models import Photo

admin.site.register(Photo)

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 manage.py createsuperuser

Username: daniel
Email address:
Password:
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).

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.

Here’s what filling the creation fields looks like.

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

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

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 views.py 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/views.py file and paste the code below:

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 views.py 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:

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:

class PhotoListView(ListView): …

class PhotoTagListView(PhotoListView):

template_name = ‘photoapp/taglist.html’

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:

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):

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:

class PhotoListView(ListView): …
class PhotoTagListView(PhotoListView): …
class PhotoDetailView(DetailView): …
class PhotoCreateView(LoginRequiredMixin, CreateView): …

class UserIsSubmitter(UserPassesTestMixin):

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
else:
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:

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:

from django.urls import path

from .views import (
PhotoListView,
PhotoTagListView,
PhotoDetailView,
PhotoCreateView,
PhotoUpdateView,
PhotoDeleteView
)

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

app_name = ‘photo’

urlpatterns = [
path(”, PhotoListView.as_view(), name=’list’),

path(‘tag//’, PhotoTagListView.as_view(), name=’tag’),

path(‘photo//’, PhotoDetailView.as_view(), name=’detail’),

path(‘photo/create/’, PhotoCreateView.as_view(), name=’create’),

path(‘photo//update/’, PhotoUpdateView.as_view(), name=’update’),

path(‘photo//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:
photo:<>

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 manage.py startapp users

INSTALLED_APPS = [

‘taggit’,

‘photoapp’,
‘users’,
]

Next, we create the urls.py file as we did with the photo app:
cd users/
touch urls.py

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

urlpatterns = [
path(‘admin/’, admin.site.urls),

path(”, include(‘photoapp.urls’)),

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:

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(
username=form.cleaned_data[“username”],
password=form.cleaned_data[“password1”],
)

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:

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:

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:
user:<>

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/settings.py file:

USE_TZ = True

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:

TEMPLATES = [
{

‘DIRS’: [BASE_DIR / ‘templates’],
‘APP_DIRS’: True,

},
]

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 manage.py file is located) and touch the base.html and navbar.html templates:
ls

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:







Django Photo Sharing app




{% include ‘navbar.html’ %}

{% 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:

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

Below is what’s presented 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:

INSTALLED_APPS = [

‘taggit’,
‘crispy_forms’,

‘photoapp’,
‘users’,
]

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/views.py:
‘photoapp/list.html’
‘photoapp/taglist.html’
‘photoapp/detail.html’
‘photoapp/create.html’
‘photoapp/update.html’
‘photoapp/delete.html’

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:

{% extends ‘base.html’ %}

{% block body %}

{% for photo in photos %}

{% 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.

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

{% extends ‘photoapp/list.html’ %}

{% block body %}

Photos with the tag {{tag}}

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

Remember that the taglist URL has the following form:
‘localhost://8000/tag//’

Here, 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:

{% extends ‘base.html’ %}

{% block body %}

{{ photo.title }}

Uploaded on: {{photo.created}}
By {{photo.submitter.username}}

{% if user == photo.submitter %}

Update
Delete

{% endif %}

More about this photo:

{{ photo.description }}

{% endblock body %}

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

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:

{% extends ‘base.html’ %}
{% load crispy_forms_tags %}

{% block body %}

Add photo

{% csrf_token %}
{{ form|crispy }}

{% 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.

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

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:

{% extends ‘base.html’ %}
{% load crispy_forms_tags %}

{% block body %}

Edit photo {{photo}}

{% csrf_token %}
{{ form|crispy }}

{% endblock body %}

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

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:

{% extends ‘base.html’ %}

{% block body %}

You are going to delete: “{{ photo }}

Are you sure, you want to delete the photo ?


{% csrf_token %}

This action is irreversible

{% endblock body %}

The deletion page would look like this.

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:

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:

{% extends ‘base.html’ %}
{% load crispy_forms_tags %}
{% block body %}

{% csrf_token %}
{{ form|crispy }}

{% comment %} Already Registered {% endcomment %}

Already Registered?
Login

{% endblock body %}

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

Last but not least, write the login template:

{% extends ‘base.html’ %}
{% load crispy_forms_tags %}

{% block body %}

{% csrf_token %}
{{ form|crispy }}

{% comment %} Already Registered {% endcomment %}

Don’t have an account?
Create account

{% endblock body %}

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!

Coded at

Share your love

69 Comments

  1. Kathaleen Akinyooye
    Kathaleen Akinyooye

    Between us, in my opinion, this is obvious. I found the answer to your question in google.com

  2. Great post! I read it with great pleasure. Now I will visit your blog more often.

  3. 5-point – C grade.

  4. Do you understand me?

  5. On our site you can create your personal horoscope for a specific day or a month in advance. We can say with precision which professions are suitable for you, and where you will succeed and your career growth.

  6. And yet it seems to me that you need to think carefully about the answer … Such questions cannot be solved in a rush!

  7. This option does not suit me. Maybe there are more options?

  8. I agree, this is a funny phrase.

  9. I am familiar with this situation. You can discuss.

  10. Yes indeed. And I ran into this.

  11. Very good message

  12. A very funny thought

  13. I read it – I liked it, thanks.

  14. In my opinion, you are wrong. I propose to discuss it. Email me at PM, we’ll talk.

  15. Not a bad post, but a lot too much.

  16. Thank you very much. Very useful information

  17. Congratulations, this is just a great thought.

  18. Thank you so much! and more posts on this topic will be in the future? Looking forward to it! zpr.

  19. Kathaleen Delgatto
    Kathaleen Delgatto

    I will run into a style of presentation

  20. Fuuuuu …

  21. Interesting 🙂

  22. Christene Blatnick
    Christene Blatnick

    How many people come to you. I envy white envy.

  23. What a good phrase

  24. I specially registered on the forum to thank you for your support.

  25. I noticed a tendency that a lot of inadequate comments appeared on blogs, I can’t understand if someone is spamming it like that? And why, to someone to make a bastard))) IMHO stupid …

  26. Bradley Fearheller
    Bradley Fearheller

    wow nice..

  27. All of the above is true. Let’s discuss this issue. Here or at PM.

  28. Quick answer, a sign of quick wits;)

  29. Continue as well.

  30. You have hit the spot. I think this is a very good idea. I completely agree with you.

  31. The blog is super, everyone would be like that!

  32. I’m sorry, this doesn’t quite suit me. Who else can suggest?

  33. All of the above is true. Let’s discuss this issue.

  34. An incomparable topic, I’m very interested))))

  35. I used to think differently, thanks for the help in this matter.

  36. Very, very good !!!

  37. There is something in this. Thank you for your help in this matter, how can I thank you?

  38. Wonderful, very valuable information

  39. Any other options?

  40. I didn’t quite understand what you meant by that.

  41. Not a bad site, especially the design

  42. I liked your blog, especially the design

  43. Perhaps I will refuse))

  44. An intelligible answer

  45. Would shake hands with the author, and give all his haters in the face.

  46. There is something in this. Got it, thanks for your help on this issue.

  47. RUBBISH !!!!!!!!!!!!!!!!!!!!!!!!!

  48. Christiane McCullough
    Christiane McCullough

    Excellent website. A lot of helpful information here. I’m sending it to several friends ans also sharing in delicious. And of course, thanks to your sweat!

  49. Very shortly this site will be famous among all blogging and site-building people, due to it’s good articles

  50. Usually I do not learn post on blogs, however I wish to say that this write-up very forced me to take a look at and do so! Your writing taste has been surprised me. Thanks, very great post.

  51. I know this if off topic but I’m looking into starting my own blog and was curious what all is needed to get setup? I’m assuming having a blog like yours would cost a pretty penny? I’m not very internet savvy so I’m not 100% positive. Any recommendations or advice would be greatly appreciated. Appreciate it

  52. I have been exploring for a little bit for any high quality articles or blog posts in this sort of space . Exploring in Yahoo I finally stumbled upon this web site. Studying this info So i’m happy to convey that I’ve a very excellent uncanny feeling I discovered exactly what I needed. I such a lot undoubtedly will make certain to don?t omit this site and provides it a look on a relentless basis.

  53. I just like the helpful info you supply to your articles. I’ll bookmark your blog and test once more right here frequently. I am slightly certain I’ll learn plenty of new stuff right right here! Good luck for the following!

  54. Fuck it!

  55. Greetings! Very useful advice within this post! It’s the little changes that will make the largest changes. Thanks a lot for sharing!

  56. Great Content on This Website

  57. always i used to read smaller articles that as well clear their motive, and that is also happening with this article which I am reading at this time.

  58. I absolutely agree with you. There is something about that and it’s a good idea. I support you.

  59. Delilah Cunningham
    Delilah Cunningham

    Hi would you mind sharing which blog platform you’re using? I’m looking to start my own blog in the near future but I’m having a tough time deciding between BlogEngine/Wordpress/B2evolution and Drupal. The reason I ask is because your layout seems different then most blogs and I’m looking for something completely unique. P.S My apologies for being off-topic but I had to ask!

  60. Hello there! Do you know if they make any plugins to assist with Search Engine Optimization? I’m trying to get my blog to rank for some targeted keywords but I’m not seeing very good success. If you know of any please share. Kudos!

  61. Why are there so few topics on the blog about the crisis, you don’t care about this question?

  62. Name Change Procedure in Hyderabad
    Name Change Procedure in Hyderabad

    Very interesting, thank you

  63. Name Change Consultants in Hyderabad
    Name Change Consultants in Hyderabad

    Excellent Information VLN Legal Services Name Change Consultants Hyderabad

  64. I want to put the advertisements in blogger, so I can get payments?.

  65. You truly did more than visitors’ expectations. Thank you for rendering these helpful, trusted, edifying and also cool thoughts on the topic to Kate.

  66. Hello there! This is my first comment here, so I just wanted to give a quick shout out and say I genuinely enjoy reading your articles. Can you recommend any other blogs/websites/forums that deal with the same subjects? Thanks.

  67. Woah! I’m enjoying the template/theme of this website. It’s simple, yet effective. A lot of times it’s very hard to get that “perfect balance” between superb usability and visual appeal. I must say you’ve done a very good job with this.

  68. Appreciation for really being thoughtful and also for deciding on certain marvelous guides most people really want to be aware of.

  69. Psychologists Mix
    Psychologists Mix

    Thanks a lot very much for the high quality and results-oriented help. I won’t think twice to endorse your blog post to anybody who wants and needs support about this area.

Leave a Reply