Many of the applications you use every day are timezone sensitive. Facebook, YouTube, Instagram, and even LateefLab are all timezone sensitive web application. 3pm in Washington D.C. is actually 12pm in Los Angeles. Django supports pytz, so we can use the module to make our application timezone aware. We're going to create a simple blog app that'll demonstrate the capabilities of Django's timezone support.
In this tutorial we're going to:
- Create Django project
- Create models.py and edit admin.py
- Creating middleware.py file
- Setting up the views.py and urls.py
- Creating a template with timezone aware date objects
- Run migrations, create superuser, and make a blog post
1. Create Django Project
You'll need to create a folder for our project (Note: Use a virtual environment such as venv or conda. This will separate). Create a folder on your desktop, call it djangoTimezoneTutorial. In terminal/CMD, enter the following:
$ cd djangoTimezoneTutorial #changes directory
$ django-admin startproject tzTutorial #creates a directory titled "tzTutorial". This is your Django project
Once the Django project has been created, open the folder titled tzTutorial. There will be a file called settings.py. Open it, and scroll down until you find a variable named USE_TZ. Make sure the value is set to "True".
2. Create a model
To demonstrate dynamic date and time objects in Django, we need to create a model with datetime aware objects. Let's create a Django app for our project.
In terminal/CMD, enter the following:
$ python manage.py startapp tzapp
Open up the tzapp folder, open the file named models.py. Copy the following models.py:
class Blogpost(models.Model):
title = models.CharField(max_length=200, unique=True)
slug = models.SlugField(max_length=200, unique=True)
author = models.ForeignKey(User, on_delete= models.CASCADE,related_name='blog_posts')
content = models.TextField()
created_on = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.title
Inside the same folder, open admin.py. Later, we'll use the built-in Django admin page to submit our blog post. Copy and paste the following to your admin.py file:from django.contrib import admin
from .models import Blogpost
class BlogpostAdmin(admin.ModelAdmin):
list_display = ('title', 'slug','created_on')
search_fields = ['title', 'content']
prepopulated_fields = {'slug': ('title',)}
admin.site.register(Blogpost, BlogpostAdmin)
3. Create middleware.py
Inside the tzapp folder, create a new file named middleware.py. This file will hold the logic that takes care of the timezone activation for our website. Copy the following into middleware.py:
import pytz
from django.utils import timezone
class TimezoneMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
tzname = request.session.get('django_timezone')
if tzname:
timezone.activate(pytz.timezone(tzname))
else:
timezone.deactivate()
return self.get_response(request)
Also, lets install the app we created inside settings.py. Under installed apps, add tzapp and activate our app's middleware.py, just like the following:INSTALLED_APPS = [
'lateeflabapp.apps.LateeflabappConfig',
'blogapp.apps.BlogappConfig',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
#'cart', #Django-cart,
'django_summernote', #WYSIWYG Editor
'django.contrib.sites', # Needed only to use sitemap framework
'django.contrib.sitemaps', #Sitemap framework; good for SEO
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'whitenoise.middleware.WhiteNoiseMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'blogapp.middleware.TimezoneMiddleware', #custom middleware, for timezone support
]
4. Create a View
Inside our tzapp folder, open views.py. We need to create a view for our blog. Copy the following:
from django.shortcuts import render, redirect, reverse
from django.views import generic
from .models import Blogpost
from django.contrib import messages
import pytz
def home(request):
#If the browser's timezone isn't set, ask user to set timezone
if request.session.get('django_timezone') == None:
return render(request, 'tzapp/settimezone.html', {'timezones': pytz.common_timezones})
else:
queryset = Blogpost.objects.all()
return render(request, 'tzapp/home.html', {'post':queryset})
def settimezone(request):
if request.method == 'POST':
#Sets the request session's timezone to the user selected timezone,
# so all datetime objects will be rendered in the user's desired timezone
request.session['django_timezone'] = request.POST['timezone']
return redirect(reverse("home"))
else:
return render(request, 'tzapp/settimezone.html', {'timezones': pytz.common_timezones})
return render(request, 'tzapp/settimezone.html', {'timezones': pytz.common_timezones})
class PostDetail(generic.DetailView):
model = Blogpost
template_name = 'tzapp/post_detail.html'
There is a good amount of things going on in our views.py. First, the "home" view, which is intended to our landing page, has a condition. If the timezone isn't set in the user's session, Django will redirect our user to the timezone selection template. Otherwise, the user will continue on to our landing page.
Second, we have our settimezone view. This function will make an HTTP Post request to our server, sending the name of the selected timezone back to the server and sets the user session's django_timezone variable.
Lastly, we create a class based view named "PostDetail". Using a class based view is different than a function based view and has many advantages in comparison to function based views. However, this tutorial won't go into the differences between the two. The purpose of this class based function is to utilize it's detail view features. Using the DetailView, we can create a basic view with only defining the HTML template and a model.
Now, we need to setup the urls.py for both our project and app. Inside the project folder (tzTutiorial), open urls.py. Copy and paste the following:
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('tzapp.urls')), # urls for our app
]
Next, we need to setup the urls.py for our app. Open our app folder (tzapp) and create a new file, name it urls.py. Add the following code to our app's urls.py:
from django.urls import path
from . import views
urlpatterns = [
path('',views.home, name='home'),
path('<slug:slug>/', views.PostDetail.as_view(), name='post_detail'),
path('settimezone', views.settimezone, name='settimezone')
]
5. Templates
Each view/url for our app needs a corresponding template, so lets create them. First, lets create the correct folder structure within our project's app folder. Inside of our app (tzapp) folder, create a folder named "templates" Inside of that folder, create another folder named tzapp (Note: the name of this folder needs to be the same name as our app). This is the folder that'll contain all of our HTML templates.
First, lets create our base.html. Django developers like to create a base.html as we use it to hold all the site-wide header HTML code, such as stylesheets and JavaScript source files. In the newly created folder, create a file named "base.html" Add the following code:
<!DOCTYPE html>
<html lang="en">
<head>
{% load static %}
<!-- Latest compiled and minified CSS -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/css/
bootstrap.min.css">
<!-- Font Awesome (free icons) -->
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.7.0/css/all.css"
integrity="sha384-lZN37f5QGtY3VHgisS14W3ExzMWZxybE1SJSEsQp9S+oqd12jhcu+A56Ebc1zFSJ"
crossorigin="anonymous">
<title>{% block title %}LateefLab TzTutorial{% endblock title %}</title>
</head>
<body>
</style>
<header>
{# Navigation Menu #}
</header>
{# The Document Body #}
<div class="container">
{% block content %}
if you see this, something is wrong!
{% endblock content %}
</div>
{# The Footer #}
<div class="footer">
</div>
<!-- Optional JavaScript -->
<!-- jQuery first, then Popper.js, then Bootstrap JS -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js">
</script>
<!-- Popper JS -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.16.0/umd/popper.min.js">
</script>
<!-- Latest compiled JavaScript -->
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js">
</script>
</div>
</body>
</html>
Next, we'll add in the landing page that'll contain the front facing HTML. Inside the same folder you created base.html, create a new file named home.html. Copy and paste the following code in your text editor:
{% extends "tzapp/base.html" %}
{% block content %}
<div class="container">
<div class="row">
<!-- Blog Entries Column -->
<div class="col-md-10 col-lg-8 col-xl-7">
<!-- Post preview-->
{% for posts in post %}
<div class="post-preview">
<a href="{% url 'post_detail' posts.slug %}">
<h2 class="post-title">{{ posts.title }}</h2>
<h3 class="post-subtitle">{{posts.subheading|slice:":100"|safe }}</h3>
</a>
<p class="post-meta">{{ posts.author }} | {{ posts.created_on}}</p>
</div>
<!-- Divider-->
<hr class="my-4" />
{% endfor %}
</div>
</div>
</div>
{%endblock%}
Now we'll create another file named post_detail.html. This template will utilize the power of Django's class based view (the one we created earlier). Combined with the url we defined in tzapp/urls.py on line 6, we can display all the attributes of our blogpost model in a more streamlined in comparison to using a function based view! Copy and paste the following code in post_detail.html:
{% extends "tzapp/base.html" %}
{% block content %}
<!-- Page Header-->
<header class="masthead">
<div class="container h-100">
<div class="container position-relative px-4 px-lg-5">
<div class="row gx-4 gx-lg-5 justify-content-center">
<div class="col-md-10 col-lg-8 col-xl-7">
<div class="site-heading">
<h1 class="text-white font-weight-bold">
{% block title %} {{ object.title }} {% endblock title %}</h1>
<span class="subheading">{{ object.author }} | {{ object.created_on }}</span>
</div>
</div>
</div>
</div>
</div>
</header>
<div class="container pt-5" >
<div class="row">
<div class="col card mb-4 mt-3 left top">
<div class="card-body">
<p></p>
<p >{{ object.content|safe }}</p>
</div>
</div>
</div>
</div>
{% endblock content %}
Last but definitely not least, create another file named settimezone.html inside the templates folder. This is the template that a user will interact with to set their timezone (if they haven't done so). Copy and past the following code for settimezone.html:
{# HTML5 declaration #}
<!DOCTYPE html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Latest compiled and minified CSS -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/css/
bootstrap.min.css">
<!-- Font Awesome (free icons) -->
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.7.0/css/all.css"
integrity="sha384-lZN37f5QGtY3VHgisS14W3ExzMWZxybE1SJSEsQp9S+oqd12jhcu+A56Ebc1zFSJ"
crossorigin="anonymous">
<title>{% block title %}Pick Your Timezone{% endblock title %}</title>
</head>
<body>
<header>
{# Navigation Menu #}
</header>
{# The Document Body #}
<div class="container-fluid p-0">
{% block content %}
<!--if you see this, something is wrong!-->
{% load tz %}
{% get_current_timezone as TIME_ZONE %}
<div class="container" style="text-align: center; padding-top: 10%;">
<h1 class= "pb-5">Set Your Timezone</h1>
<div class="row pt-5 pb-5 justify-content-center">
<form action="{% url 'settimezone' %}" method="POST" class="form-inline">
{% csrf_token %}
<label for="timezone">Time zone:</label>
<select name="timezone" class="form-control mr-3 ">
{% for tz in timezones %}
<option value="{{ tz }}"{% if tz == TIME_ZONE %}
selected{% endif %}>{{ tz }}</option>
{% endfor %}
</select>
<input type="submit" value="Set" class="form-control btn btn-success">
</form>
</div>
</div>
{% endblock content %}
</div>
{# The Footer #}
<!-- Optional JavaScript -->
<!-- jQuery first, then Popper.js, then Bootstrap JS -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"><
/script>
<!-- Popper JS -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.16.0/umd/popper.min.js">
</script>
<!-- Latest compiled JavaScript -->
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js">
</script>
</body>
</html>
6. Run migrations, create superuser, and make a blog post!
We're nearly finished with setting up the project! It'll be a good idea to run migrations before we create a superuser and run the project. Inside of your terminal/CMD, run the following commands from your project's root folder (tzTutorial; same folder level as settings.py):
$ python manage.py makemigrations
$ python manage.py migrate
While we're still in the terminal/CMD, lets create a super user. Run the following command in your terminal/CMD:
$ python manage.py createsuperuser
Follow the prompt, providing a username, email address, and password. Once finished, now lets run the project!
In your terminal/CMD, run the following command to start the project:
$ python manage.py runserver
Go to http://localhost:8000 , you'll be greeted by the settimezone.html!:
Once you set your timezone, you'll see an error such as this:
We get this error because we haven't created any blogposts (also this tutorial doesn't handle this particular error of no blogposts). Lets create a blogpost! Go to http://localhost:8000/admin and sign in with the same superuser credentials you did earlier.
Once signed in, click "Blogposts". Now click "Add Blogpost" button. Fill out the model (don't forget to select yourself as the author).
After you submitted, go back to http://localhost:8000.You should see the post you just created!
To demonstrate the timezone, lets delete our cookies so the next time we go to the homepage, it'll ask us to set our timezone. I'm in the US/Eastern (UTC-5) timezone, but i'll switch to US/West(UTC-8). So when I reset my timezone, it should show 8:40pm.
And there you have it. Now you're setting up timezone support in your Django projects! Check out the Github repo: tzTutorial Repo