Creating a Travel Diary With Django
You started learning Django by creating a blog from the DG tutorial at a Django Girls event near you. This is the first python event youāve been to and you fell in love with both the language and the community. Now you have your first application you want to learn how to do more and improve your skills.
This blog post narrates the process of implementing an interactive map and how to embed said map into Django website. Youāll learn how to create relationships between tables through foreign keys. Youāll also learn how to filter objects in the database using the Django ORM and create specific pages to show the filtered objects.
Note: You can alsoĀ skip ahead to the section Storing Spacial Data if you already have the blog from Django Girls tutorial.
The code shown on this tutorial is also available in this GitHub repository.
Putting together the Django Girls Tutorial blog
You can go to theĀ Django Girls Tutorial hereĀ to learn in more detail how to put together the blog but below youāll see the steps Iāve taken with some minor edits of my own for simplicity sake.
Getting started: Set Up
Creating the python environment, installing Django, and creating the blog app:
python -m venv .env
source .env/bin/activate# On Windows: .env\Scripts\activate
pip install django
django-admin startproject travel_diaries .
python manage.py startapp blog
The first thing to note is that by default Django uses SQLite as the default database, you can check that by looking inĀ travel_diaries/settings.py
Ā and looking for theĀ DATABASES
Ā variable and you should see something like this:
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": BASE_DIR / "db.sqlite3",
}
}
After creating the blog app you should add it to theĀ INSTALLED_APPS
Ā list inĀ travel_diaries/settings.py
:
INSTALLED_APPS = [
# ...
'blog',
]
I also updated the timezone and the language settings in the same file:
LANGUAGE_CODE = "en-ca"
TIME_ZONE = "America/Toronto"
Creating Superusers
One last thing to do before running the app: creating a user with admin powers.
To do so, you need to run the migrations already created by default when you start your project. This creates the basic structure Django needs to create users for accessing theĀ /admin
, is through theĀ /admin
Ā that you can create blog posts.
python manage.py migrate
python manage.py createsuperuser
Then run the server:
python manage.py runserver
Now you can also access the admin page and login atĀ http://127.0.0.1:8000/admin
.
Modeling a Post
The only thing you can do at this point is manage and create users. Letās change that.
For the purpose of this tutorial youāll only have one user that is you, and youāll be the author of the blog posts, so letās move on to modeling a post. First create the blog app:
python manage.py startapp blog
Then add the following toĀ blog/models.py
from django.db import models
from django.conf import settings
from django.utils import timezone
class Post(models.Model):
author = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT)
title = models.CharField(max_length=200)
content = models.TextField()
created_at = models.DateTimeField(default=timezone.now)
published_date = models.DateTimeField(blank=True, null=True)
def publish(self):
self.published_date = timezone.now()
self.save()
def __str__(self):
return self.title
Note that, differently from the Django Girls Tutorial, here we are protecting authors upon deletion of blog posts by using on_delete=models.Protect
, this avoids the author being deleted when a blog post is deleted.
Then remember to register your new model in the admin so you can see it inĀ /admin
Ā and be able to create blog posts, to do so openĀ blog/admin.py
Ā and update the file like so:
from django.contrib import admin
from .models import Post
admin.site.register(Post)
If you have theĀ /admin
Ā page open just refresh it and you should see your Post
model in there, if not run your server and login to see it.
Creating Posts
Before you can actually create a post you need to create the corresponding tables on the database, do that by running the two commands below:
python manage.py makemigrations
python manage.py migrate
The makemigrations
command creates scripts to update the database called migrations, since this is a new model, the script will create a new table to store the data for each new post. Then the migrate
command actually run the migrations and creates the tables and other updates.
Through theĀ /admin
Ā interface you should be able to create your blog posts. Take some time to add a couple so you can have some data, this will be used later too.
Listing Blog Posts in a Page
Time to create HTML and CSS files so we can list our posts and render the posts beautifully.
- Inside your
blog/
folder, create aĀtemplates/
Ā folder and then aĀblog/
Ā folder inside templates; -
Now create aĀ
base.html
Ā with the following code:{% load static %} <!DOCTYPE html> <head> <title>{% block title %}Travel Diaries{% endblock %}</title> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous"> <link rel="stylesheet" href="{% static 'css/blog.css' %}"> </head> <body> <header class="page-header"> <div class="container"> <h1><a href="/">Travel Diaries</a></h1> </div> </header><br><br> <main class="container"> <div class="row"> <div class="col"> {% block content %} {% endblock %} </div> </div> </main> </body> </html>
This base template will hold all the bits of HTML you can reuse across all other pages that extend it.
-
Create also aĀ
post_list.html
Ā like so:{% extends 'blog/base.html' %} {% block content %} {% for post in posts %} <article> <h2><a href="{% url 'post_detail' pk=post.pk %}">{{ post.title }}</a> <small> ā¢ <time>published: {{ post.published_date }}</time></small></h2> </article> {% endfor %} {% endblock %}
Note: This post list is a bit different from that on the Django Girls tutorial as it only displays the Post title and publishing date.
-
And finallyĀ
post_detail.html
:{% extends 'blog/base.html' %} {% block content %} <article> <h2>{{ post.title }}</h2> <small><time>published: {{ post.published_date }}</time></small> <br><br> <p>{{ post.content | safe | linebreaks }}</p> </article> {% endblock %}
Note: This post detail is also a bit different from that on the Django Girls tutorial as it not only displays the Post content but also allow for rendering HTMLs that might be part of the post like the embedded code for a YouTube video for example.
-
Now you may have noticed the usage of a CSS file in theĀ
base.html
, but you currently donāt have that file, so letās us create it, create aĀstatic/
Ā folder inside your blog app folder and in it aĀcss/
Ā folder containing aĀblog.css
Ā file with the following content:h1 a, h2 a { color: #c175ff; }
Make sure to change the color you see there as a hex code to one of your liking.
-
Now you need a view or better views to use these amazing templates. So updateĀ
blog/views.py
Ā to this:from django.shortcuts import render, get_object_or_404 from django.utils import timezone from .models import Post def post_list(request): posts = Post.objects.filter(published_date__lte=timezone.now()).order_by('-published_date') return render(request, 'blog/post_list.html', {"posts": posts}) def post_detail(request, pk): post = get_object_or_404(Post, pk=pk) return render(request, 'blog/post_detail.html', {"post": post})
TheĀ
post_list
Ā function will grab all posts you have order them by the publish date putting the most recent blog post first in theposts
. This filter returns aĀQuerySet
Ā that gets injected into the page when therender()
is called. You can think of thisĀQuerySet
Ā as a list resulting of filtering the posts in the database.TheĀ
post_detail
Ā function on the other hand, is responsible for rendering a given post in its own page, it finds the post if it exists using its primary key and injects that through the render function. -
To make use of the HTMLs and Views you need to update the URLs so your application know how to render the blog pages, so create theĀ
urls.py
Ā file within your blog app and update theĀblog/urls.py
Ā like this:from django.urls import path from . import views urlpatterns = [ path('', views.post_list, name='post_list'), path('post/<int:pk>', views.post_detail, name='post_detail'), ]
The first pattern uses theĀ
post_list.html
Ā and corresponding homonyms view to render the list of posts while the second is responsible for generating a page for each individual post. Note that the post URL for an individual post follows a pattern likepost/1
where the1
is the primary key of a post in the database.An improvement for a later date is to use the post slug for generating the URL as opposed to the primary key.
Now you can see a post like this:
Storing Spacial Data
Up until now it you had a recap of the Django Girls Tutorial with some tiny differences. If you want more details you can find it in theĀ Django Girls Tutorial.
But letās move to the fun parts! First things first, we need to make a decision:Ā should you store the location as part of the post model itself or should you create a separate object to store the information? And if you separate things in different models, should you keep things in the same app or put things in a separate app entirely?
To facilitate re-usability, since you can have more than one blog post for a given location, maybe a place you visit quite often, is better to separate the location as its own model.
With the same mindset, we should be able to keep each module as specific to the part of the application it contains, it is a good practice to separate responsibilities into different apps, having a separate app also allows you to re-use this code in other projects, so thatās what weāll do in this blog post.
Creating Map App
The app to hold the locations will be named Map and will host all logic for locations and the map generation. Splitting this allow you for example to reutilize the code at other applications with little refactor, and also for the map to exist almost independently from your blog. Letās create a new app:
python manage.py startapp map
Then add that to your application by updating theĀ travel_diaries/settings.py
:
INSTALLED_APPS = [
# ... previous installed apps
"django.contrib.staticfiles",
"blog",
"map",
]
Modeling a Location
In order to show locations both on the blog and on the map you need to be able store them so we need a model, updateĀ map/models.py
Ā like so:
from django.db import models
class Location(models.Model):
name = models.CharField(max_length=100)
latitude = models.DecimalField(max_digits=12, decimal_places=8, blank=True, null=True)
longitude = models.DecimalField(max_digits=12, decimal_places=8, blank=True, null=True)
def __str__(self):
return self.name
Put simply a Location has a name and the coordinates to find it in a map (latitude and longitude).
Since you now have a model is a good time to create the corresponding table on your database, run:
python manage.py makemigrations
python manage.py migrate
Creating a Map
Now that you have a model you need to both add a view for it, and add it to the admin so you can create new locations. Letās start with the basics: creating the view. Go to theĀ map/views.py
Ā and add the following code:
import folium
from django.http import HttpResponse
from .models import Location
def map_page(request):
center = (13.133932434766733, 16.103938729508073)
folium_map = folium.Map(location=center, zoom_start=2)
return HttpResponse(folium_map.get_root().render())
Letās breakdown what is happening:
folium
: is a library to that allows you to create maps in Python with Leafleet.js, you can check out theĀ Folium documentation hereĀ or this otherĀ blog post I wrote to learn the Folium basics (translated with the help of Google from Portuguese - for now);folium.Map
: creates a map of the world far enough to see most of the world map;folium_map.get_root().render()
: exports the created map into an HTML as a string that can be passed to the view.
After updating theĀ map/views.py
Ā install Folium like this:
pip install folium
Define the URL patterns for map app in theĀ map/urls.py
Ā like this:
from django.urls import path
from . import views
urlpatterns = [
path('', views.map_page, name='map'),
]
And add the map app to the application URLs in theĀ travel_diaries/urls.py
Ā like this:
# ... pre-existing imports
urlpatterns = [
path("admin/", admin.site.urls),
path("", include("blog.urls")),
path("map/", include("map.urls")), # š added code
]
With these alterations every time someone accessĀ /map
Ā endpoint on your site they will see the map created.
Double check your work: accessĀ http://127.0.0.1:8000/map
Ā to see the blank map.
Thatās cool but too ābasicā so letās add some locations and pins to our map.
Creating Locations
Now that you have a view and a model make sure to register the model on theĀ map/admin.py
Ā file in order to see locations in the admin UI:
from django.contrib import admin
from .models import Location
admin.site.register(Location)
If your server is running, refresh your page to see the update, if not, run your server and login to the admin panel in order to create locations.
Make sure to add a few that would correspond to each of your blog posts.
Adding Locations to Blog Posts
Now since you want to associate each blog post to a given location we need to adjust the blog post model and make some improvements to the HTML templates.
Letās begin by updating the Post model in theĀ blog/models.py
Ā youāll add a new field calledĀ location
Ā and it will be aĀ ForeignKey
. This will create a link between your post in the Post table and location that exists in the Location table.
Since you want to be able to create blog posts that arenāt tied to a location, youāll also want to make this field optional by using the parametersĀ blank
Ā andĀ null
. This will also help since you want to put a default location or have to implement your own migration in order to fill the field in the database.
# other imports omitted
from map.models import Location # š new import
class Post(models.Model):
# other fields omitted
published_date = models.DateTimeField(blank=True, null=True)
# š new code
location = models.ForeignKey(
Location, related_name='place',
blank=True, null=True, # allow for blog posts without location
on_delete=models.PROTECT # keep location if a blog post gets deleted
)
# š new code
# omitted pre-existing code
Okay now letās update the templates, start with theĀ post_detail.html
. Add the following after the closingĀ </time>
Ā tag in the HTML:
{% if post.location %} ā¢ {{ post.location }}{% endif %}
This will render the location of a post in the post page if that blog post has a location filled in. Since the location is part of the post object as foreign key, you can still access it through the post object we pass in theĀ render
Ā function of the post detail view.
Make sure to add some locations through the admin pageĀ andĀ update the blog posts to have locations
Linking The Map in the Main Page
While you are editing templates might be a good time to link the Map in the footer of the base template. Open theĀ base.html
Ā and add the code below after the closing of theĀ </main>
Ā tag:
<footer class="page-footer">
<div class="container">
<br>
<p><a href="/map"> Map </a></p>
</div>
</footer>
Finally, you may have noticed that this hyperlink is rendering in that awful blue color so update the CSS file so that this hyper link also renders in your chosen color, replace the content ofĀ blog/static/css/blog.css
Ā with this:
h1 a, h2 a, p a {
color: #c175ff;
}
This way you can see your map with the same color setting for all other links in your page like this:
Showing Locations on a Map
Okay now that you have locations you should be able to see them in the map, right? Wrong! Our map has no information to display yet, to change that you need to update the map view. Add this code to it:
# other imports omitted
from django.http import HttpResponse
from .models import Location
def map_page(request):
center = (13.133932434766733, 16.103938729508073)
folium_map = folium.Map(location=center, zoom_start=2)
# š new code
locations = Location.objects.all()
for location in locations:
popup = f"{location.name}"
folium.Marker(
location=[location.latitude, location.longitude],
popup=popup
).add_to(folium_map)
# š new code
return HttpResponse(folium_map.get_root().render())
This extra code does the following:
- Queries all the locations in the database withĀ
Location.objects.all()
; - Then iterates over the QuerySet of locations to add aĀ
folium.Marker
Ā using the name and coordinates for each location.
After adding some locations you should be able to accessĀ http://127.0.0.1:8000/map
Ā and see a map with the locations you created with their corresponding pins.
Creating Location Pages
Now this is all fun, letās make it moreĀ complete. One fun thing to do using views is to generate pages based on the data you have stored in the database. So letās create a new view to get a list of posts for a given location, add the code below on map/views.py
:
# other imports omitted
from django.utils import timezone
from blog.models import Post
# other views omitted
def post_list_by_location(request, name):
location = Location.objects.get(name__iexact=name)
posts = Post.objects.filter(
location=location, published_date__lte=timezone.now()
).order_by('-published_date')
return render(
request, 'map/post_list_by_location.html',
{"posts": posts, "location": location}
)
Lets breakdown the code:
Location.objects.get(name__iexact=name)
: filters location based on the name without caring for capitalization;- Then we get all the posts usingĀ
Post.objects.filter
Ā passing the location from the previous step; - Finally we pass the posts and the location to the page rendered by the templateĀ
map/post_list_by_location.html
.
Speaking of template, now that we have a new view that actually uses templates create the templates folder to host the HTML that will be used by this view like this:
mkdir -p map/templates/map/
Then create a new template in theĀ map/templates/map/
Ā folder namedĀ post_list_by_location.html
:
{% extends 'blog/base.html' %}
{% block content %}
<h2>Posts in {{ location }}</h2><br>
{% for post in posts %}
<article>
<h2><a href="{% url 'post_detail' pk=post.pk %}">{{ post.title }}</a> <small> ā¢ <time>published: {{ post.published_date }}</time></small></h2>
</article>
{% endfor %}
{% endblock %}
And to you can see the pages add the new URL patterns inĀ map/urls.py
:
# imports omitted
urlpatterns = [
path('', views.map_page, name='map'),
path('<name>', views.post_list_by_location), # š new code
]
TheĀ <name>
Ā ensures the part of the URL path that will contain the name of the location will be passed along to the view: for example accessingĀ http://127.0.0.1:8000/map/london
Ā will render the corresponding London page with the collection of posts related to London.
Linking Location Pages in the Map
One final thing: might be fun to add a tiny preview map of the location on the page so lets update the view:
# pre-existing code
def post_list_by_location(request, name):
location = Location.objects.get(name__iexact=name)
# š new code
folium_map = folium.Map(
location=[location.latitude, location.longitude],
zoom_start=4, width=400, height=300
)
folium.Marker(
location=[location.latitude, location.longitude],
popup=location.name
).add_to(folium_map)
location_map = folium_map._repr_html_()
# š new code
posts = Post.objects.filter(
location=location, published_date__lte=timezone.now()
).order_by('-published_date')
return render(
request, 'map/post_list_by_location.html',
# š updated code
{"posts": posts, "location": location, "location_map": location_map}
# š updated code
)
folium.Map
: creates a map;folium.Marker
: adds the pin to map;folium_map._repr_html_()
: generates the HTML for the map;- then finally,Ā
"location_map": location_map
Ā we pass the HTML map to be injected into the page.
And now update the template HTML fileĀ map/templates/map/post_list_by_location.html
Ā like so:
{% extends 'blog/base.html' %}
{% block content %}
<h2>Posts in {{ location }}</h2><br>
<!-- š new code -->
<div class="row" style="width:50%">
<center>
{{ location_map | safe }}
</center>
</div><br>
<!-- š new code -->
{% for post in posts %}
<article>
<h2><a href="{% url 'post_detail' pk=post.pk %}">{{ post.title }}</a> <small> ā¢ <time>published: {{ post.published_date }}</time></small></h2>
</article>
{% endfor %}
{% endblock %}
This update then render the map as a part of the page:
Now we need to add a link to this pages somewhere right? So letās use our Map. Update the view that generate the markers on the mapĀ map/views.py
Ā like so:
def map_page(request):
center = (13.133932434766733, 16.103938729508073)
folium_map = folium.Map(location=center, zoom_start=2)
locations = Location.objects.all()
for location in locations:
# š updated code
popup = f"""
<a href="/map/{location.name}">{location.name}</a>
"""
# š updated code
folium.Marker(
location=[location.latitude, location.longitude],
popup=popup
).add_to(folium_map)
folium_map.save("map/templates/map/map.html")
return render(request, "map/map.html")
And that is it, you are good to go! Go checkout theĀ /map
Ā page and try clicking on the name of one of your locations:
Recap
Wow, that was a lot! You learned or remembered how to create a blog in the style of the Django Girls tutorial with some tiny differences to make the experience more personalized. Then you got to have fun and learn how to use Folium to generate maps and embed those maps in pages on your page.
On the Django side you saw how to use foreign keys to link your posts to locations and how filter posts based on the relationship they had with that location. And then you saw how to create specific pages with the filtered list of posts.
I hope you had as much fun reading as I had writing this, if you read until here Iād love to hear/read your questions so send me a message on Bluesky.
Before you go if you want to think of next steps for you to stretch your skills I recommend you trying both saving the map once a new location is added this way you can just render the map as opposed to querying the the database every time /map
is accessed and creating a 404 page in case some tries to access locations that donāt exist.
Once again, the code shown on this tutorial is also available in this GitHub repository.
See ya on the next one.
