The advantage of a framework is that all its parts can interact with each other easily. Do you remember that Django is based on the Model-View-Template architecture? It consists of the model, which defines the data structure, the view, which handles the logic and interaction with the user, and the template, which controls the presentation and rendering of the data. In addition, we have forms to collect information from users. Let's see it in action — process and pass data from the database to our HTML templates and get it from HTML forms to save to the database.
Initial setup
Let's create our music service. This service lets you listen to various tracks, search for specific songs, and mark your favorite ones.
First, let's create our project using the following command:
django-admin startproject music_playerNow we'll create an application inside the project directory. Let's call it play.
python manage.py startapp playWe've created a new application and need to add it to the INSTALLED_APPS list in the settings.py file.
INSTALLED_APPS = [
...
'play',
]We don't need much for the prototype — let's use Django's User model and define another one ourselves in the models.py file:
from django.contrib.auth.models import User
from django.db import models
class Song(models.Model):
title = models.CharField(max_length=128)
artist = models.CharField(max_length=128)
path_to_file = models.FileField(upload_to='static/')
favorite_by = models.ManyToManyField(User, related_name='favorite_songs')All attributes of models are accessed through the dot. For example, if we create the song variable as an instance of the Song class, its attributes will be available using expressions like song.title, song.artist, etc.
The path_to_file is a relative path to a file from the upload_to directory. Don't redefine the default value for static files in the settings.py module, and use the static as a file source prefix. If we create an instance of the Song model where the value of the path_to_file field is text.txt, Django will look for the text.txt file in the static directory.
To make the static file directory work, create it in the root of your project and then define STATICFILES_DIRS in the settings.py module.
The next step is to place the music files in the static directory of our app.
Don't forget to migrate a newly created model:
python manage.py makemigrations
python manage.py migrateThe next step is creating a template directory inside the application — play/templates/play. We'll need two pages for our educational purposes: song_list.html and play_page.html.
The idea is to turn it into dynamic content generated or customized based on user requests or inputs, using server-side technologies to fetch data from a database or perform calculations before delivering the content to the user. To achieve this, we can pass a context dictionary to the template to fill it with data from our models.
A context is a dictionary with variable names as the key and their values as the value. Assume that you defined the context variable song, assigned it the value of an instance of the Song class, and want to add it to your context dictionary. Here is how to do that:
my_favorite_song = Song(title='All you need is a computer, and a little belief in yourself',
artist='Peachcake',
path_to_file='file_aync_peachcake_2005.mp3')
context = {'song': my_favorite_song}In this case, our context dictionary is {'song': my_favorite_song}. In models, all fields will be available for the template layout; you can even access foreign keys and their fields. You can pass context dictionaries to your views to make them available for your templates.
Fill the database with your favorite music tracks through the Django admin panel: create the superuser and access the panel at http://127.0.0.1:8000/admin/. To make our service work, let's create two significant components: views and URL paths. Since the primary purpose of our topic is to learn to use models with templates, we will only take a quick look at these steps.
Views
In Django, web pages and other content (like JSON, spreadsheets, or PDFs) are delivered by views. Each view generally serves a specific function and has a particular template of HTML. For example, in our playback application, we'll need at least two view classes, one for the song list and one for the pages with a music player where we can listen to our songs.
Let's add a list of songs to the views.py file:
from django.views import generic
from .models import Song
class IndexView(generic.ListView):
template_name = 'play/song_list.html'
context_object_name = 'songs'
def get_queryset(self):
return Song.objects.all()As you can see, we're forming a QuerySet of song instances from the database to pass them to the template.
The second page is a detailed view of the exact music track (play/play_page.html). The view class for it may be like this:
class MusicFileView(generic.DetailView):
template_name = 'play/play_page.html'
model = SongGreat job! To make things work, we need to tune the URL paths. Let's get to it!
URLs
Django will choose a view by examining the URL requested by the user. We'll open the urls.py file of the music_player project and use the include function for the application play:
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('play.urls')),
]Create the urls.py file in the application's directory and add the following code there:
from django.urls import path
from . import views
app_name = 'play'
urlpatterns = [
path('', views.IndexView.as_view(), name='index'),
path('<int:pk>/', views.MusicFileView.as_view(), name='music_file'),
]As you can see, we'll create a URL path for a music file using its primary key.
Forms
One of the best features of music services is the possibility to create playlists with favorite songs. Our service needs this feature! Write the template of the play_page.html page with a form to use it further in POST requests:
{% if user.is_authenticated %}
<h2>{{song.artist }} - {{ song.title }}</h2>
<audio controls>
<source src="{{ song.path_to_file }}" type="audio/mpeg">
</audio>
<form action="{% url 'play:likes' song.id %}" method="post">
{% csrf_token %}
<input type="hidden" name="song_id" value="{{ song.id }}">
<button type="submit">Add to favorites</button>
</form>
{% endif %}We access the id field of a variable with the dot. If you looked closely at the code, you've noticed we're also using the cryptic tag {% csrf_token %}. CSRF is an abbreviation for Cross-Site Request Forgery. We don't want any fraud action to happen, so in forms, we must always use this tag to secure our applications. The CSRF token is a generated sequence of symbols that the server uses to identify a user's session. If the sequence matches, the form is considered reliable. Furthermore, we added the user_is_authenticated option to see who liked an exact song and to create lists of favorites for authenticated users of our future service.
Now we have a form to send to the server. It will process the request and add the song of our choice to our favorites.
For the request to work successfully, there must be a POST request handler for the /add_to_favorites address in the application. You can find this URL in the form's action attribute.
When submitting a form with an Add to favorites button, the data will be sent to the address specified in the form's action attribute, which should match our application's /add_to_favorites address. You need to ensure that your application has a POST request handler defined for this address to handle adding a song to your favorites.
Many objects
A whole playlist will be shown on the main page, song_list.html, and it's slightly different from having just one object per page. Using a for loop and a QuerySet would work well for a situation like that.
Let's update the content of our song_list.html file, dynamically replacing the content in the body with our list of songs. Moreover, modify the get_queryset method to get ten or fewer random songs from the database. Now, our context data is {'songs': Song.objects.all()[:10]}.
{% for song in songs %}
<div>
<h4>{{ song.artist }} - {{ song.title }}</h4>
</div>
{% endfor %}Here's another excellent idea: add links to the music player from the playlist! So, let's change the previous code to open music file pages directly from here:
{% for song in songs %}
<ul>
<li><a href="{% url 'play:music_file' song.id %}">{{ song.artist }} - {{ song.title }}</a></li>
</ul>
{% endfor %}Great! But why stop here? Let's add another great feature to our server and display the number of users who added it to their favorites for each song display. To complete this, access the user's QuerySet with favorite_by attribute defined in the model and call the count method on it. As for other Python objects, the only template candidates are methods with no parameters. Count is one of them.
{% for song in songs %}
<ul>
<li><a href="{% url 'play:music_file' song.id %}">{{ song.artist }} - {{ song.title }}</a></li>
<div>{{ song.favorite_by.count }}</div>
</ul>
{% endfor %}Now the users can see which songs are popping and rocking!
It's usually better to calculate such data at the stage of processing the request in a handler to define only the representation of a page in our template, but now we can break from this rule for a better understanding of the principles of interaction between models and templates
Fine! What else do we need to make the form of adding to favorites work? Of course, we need to add a view function to views.py!
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404
from django.urls import reverse
def likes(request, id):
song = get_object_or_404(Song, id=request.POST.get('song_id'))
if song.favorite_by.filter(id=request.user.id).exists():
song.favorite_by.remove(request.user)
else:
song.favorite_by.add(request.user)
return HttpResponseRedirect(reverse('play:index'))As you can see, we don't need to use a unique page for likes; redirect it to the same page.
And we need the URL path for this action; add it to patterns in play/urls.py:
urlpatterns = [
path('', views.IndexView.as_view(), name='index'),
path('<int:pk>/', views.MusicFileView.as_view(), name='music_file'),
path('<int:id>/likes/', views.likes, name='likes'),
]Filters
In Django, filters are available for manipulating values within templates. While they can save time, they may also produce unexpected outcomes. It's important to note that the template processor relies on an object's string representation, which has advantages and disadvantages. To fully utilize Django, it's crucial to exercise caution when using filters.
Let's slightly change the template from the previous example:
{% for song in songs %}
<ul>
<li><a href="{% url 'play:music_file' song.id %}">{{ song.artist }} - {{ song.title }}</a></li>
<div>Loved by {{ song.favorite_by.all|random }}</div>
</ul>
{% endfor %}We render a random User who added a song to favorites (provided we have the user's consent). Upon receiving the QuerySet, we pass it to the random filter to get one username to show.
Though this feature is quite experimental, it still has a chance to win the hearts of our users, so let's render five of them instead of just one:
{% for song in songs %}
<ul>
<li><a href="{% url 'play:music_file' song.id %}">{{ song.artist }} - {{ song.title }}</a></li>
<div>Loved by {{ song.favorite_by.all|slice:5 }}</div>
</ul>
{% endfor %}We sliced the QuerySet and limited its length to five items: what could go wrong? Yet, when we open the browser, we get the strange output:
What kind of English is that? It seems like Django converted the QuerySet to a string and rendered it to HTML. It's not a result to be particularly proud of. Don't lose heart, though, as we have another trick up our sleeve:
{% for song in songs %}
<ul>
<li><a href="{% url 'play:music_file' song.id %}">{{ song.artist }} - {{ song.title }}</a></li>
<div>Loved by {{ song.favorite_by.all|slice:5|unordered_list }}</div>
</ul>
{% endfor %}This time, we prudently convert all values to the unordered HTML list with the unordered_list filter, and our output now looks quite satisfying:
Well, there are plenty of ideas to expand our service with more options, but let's stop for now and continue practicing Django in the tasks for this topic!
Conclusion
While working with our music player, we've got in touch with the principles of Django models' use in HTML templates.
Let's summarize what we've learned:
The way we can pass objects from the database to the template;
How to access the model's attributes for different purposes;
How to represent organized lists of objects, how to sort them and use filters;
Finally, we've seen some examples of forms, view functions, and URL patterns we can create to represent data from the database.