Build Python Django apps with Microsoft Graph
This tutorial teaches you how to build a Python Django web app that uses the Microsoft Graph API to retrieve calendar information for a user.
Tip
If you prefer to just download the completed tutorial, you can download it in two ways.
- Download the Python quick start to get working code in minutes.
- Download or clone the GitHub repository.
Prerequisites
Before you start this tutorial, you should have Python (with pip) installed on your development machine. If you do not have Python, visit the previous link for download options.
You should also have either a personal Microsoft account with a mailbox on Outlook.com, or a Microsoft work or school account. If you don't have a Microsoft account, there are a couple of options to get a free account:
- You can sign up for a new personal Microsoft account.
- You can sign up for the Office 365 Developer Program to get a free Office 365 subscription.
Note
This tutorial was written with Python version 3.9.0 and Django version 3.1.4. The steps in this guide may work with other versions, but that has not been tested.
Feedback
Please provide any feedback on this tutorial in the GitHub repository.
Create a Python Django web app
In this exercise you will use Django to build a web app.
If you don't already have Django installed, you can install it from your command-line interface (CLI) with the following command.
pip install --user Django==3.1.4Open your CLI, navigate to a directory where you have rights to create files, and run the following command to create a new Django app.
django-admin startproject graph_tutorialNavigate to the graph_tutorial directory and enter the following command to start a local web server.
python manage.py runserverOpen your browser and navigate to
http://localhost:8000. If everything is working, you will see a Django welcome page. If you don't see that, check the Django getting started guide.Add an app to the project. Run the following command in your CLI.
python manage.py startapp tutorialOpen ./graph_tutorial/settings.py and add the new
tutorialapp to theINSTALLED_APPSsetting.INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'tutorial', ]In your CLI, run the following command to initialize the database for the project.
python manage.py migrateAdd a URLconf for the
tutorialapp. Create a new file in the ./tutorial directory namedurls.pyand add the following code.from django.urls import path from . import views urlpatterns = [ # / path('', views.home, name='home'), # TEMPORARY path('signin', views.home, name='signin'), path('signout', views.home, name='signout'), path('calendar', views.home, name='calendar'), ]Update the project URLconf to import this one. Open ./graph_tutorial/urls.py and replace the contents with the following.
from django.contrib import admin from django.urls import path, include from tutorial import views urlpatterns = [ path('', include('tutorial.urls')), path('admin/', admin.site.urls), ]Add a temporary view to the
tutorialsapp to verify that URL routing is working. Open ./tutorial/views.py and replace its entire contents with the following code.from django.shortcuts import render from django.http import HttpResponse, HttpResponseRedirect from django.urls import reverse from datetime import datetime, timedelta from dateutil import tz, parser def home(request): # Temporary! return HttpResponse("Welcome to the tutorial.")Save all of your changes and restart the server. Browse to
http://localhost:8000. You should seeWelcome to the tutorial.
Install libraries
Before moving on, install some additional libraries that you will use later:
- Microsoft Authentication Library (MSAL) for Python for handling sign-in and OAuth token flows.
- Requests: HTTP for Humans for making calls to Microsoft Graph.
- PyYAML for loading configuration from a YAML file.
- python-dateutil for parsing ISO 8601 date strings returned from Microsoft Graph.
Run the following command in your CLI.
pip install msal==1.7.0 pip install requests==2.25.0 pip install pyyaml==5.3.1 pip install python-dateutil==2.8.1
Design the app
Create a new directory in the ./tutorial directory named
templates.In the ./tutorial/templates directory, create a new directory named
tutorial.In the ./tutorial/templates/tutorial directory, create a new file named
layout.html. Add the following code in that file.<!DOCTYPE html> <html> <head> <title>Python Graph Tutorial</title> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/css/bootstrap.min.css" integrity="sha384-TX8t27EcRE3e/ihU7zmQxVncDAy5uIKz4rEkgIXeMed4M0jlfIDPvg6uqKI2xXr2" crossorigin="anonymous"> <link rel="stylesheet" href="https://static2.sharepointonline.com/files/fabric/office-ui-fabric-core/11.0.0/css/fabric.min.css" /> {% load static %} <link rel="stylesheet" href="{% static 'tutorial/app.css' %}"> </head> <body> <nav class="navbar navbar-expand-md navbar-dark fixed-top bg-dark"> <div class="container"> <a href="{% url 'home' %}" class="navbar-brand">Python Graph Tutorial</a> <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarCollapse" aria-controls="navbarCollapse" aria-expanded="false" aria-label="Toggle navigation"> <span class="navbar-toggler-icon"></span> </button> <div class="collapse navbar-collapse" id="navbarCollapse"> <ul class="navbar-nav mr-auto"> <li class="nav-item"> <a href="{% url 'home' %}" class="nav-link{% if request.resolver_match.view_name == 'home' %} active{% endif %}">Home</a> </li> {% if user.is_authenticated %} <li class="nav-item" data-turbolinks="false"> <a class="nav-link{% if request.resolver_match.view_name == 'calendar' %} active{% endif %}" href="{% url 'calendar' %}">Calendar</a> </li> {% endif %} </ul> <ul class="navbar-nav justify-content-end"> <li class="nav-item"> <a class="nav-link external-link" href="https://developer.microsoft.com/graph/docs/concepts/overview" target="_blank"> <i class="ms-Icon ms-Icon--NavigateExternalInline mr-1" aria-hidden="true"></i>Docs </a> </li> {% if user.is_authenticated %} <li class="nav-item dropdown"> <a class="nav-link avatar-link dropdown-toggle" data-toggle="dropdown" href="#" role="button" aria-haspopup="true" aria-expanded="false"> {% if user.avatar %} <img src="{{ user.avatar }}" class="rounded-circle align-self-center mr-2" style="width: 32px;"> {% else %} <img src="{% static 'tutorial/no-profile-photo.png' %}" class="rounded-circle align-self-center mr-2" style="width: 32px;"> {% endif %} </a> <div class="dropdown-menu dropdown-menu-right"> <h5 class="dropdown-item-text mb-0">{{ user.name }}</h5> <p class="dropdown-item-text text-muted mb-0">{{ user.email }}</p> <div class="dropdown-divider"></div> <a href="{% url 'signout' %}" class="dropdown-item">Sign Out</a> </div> </li> {% else %} <li class="nav-item"> <a href="{% url 'signin' %}" class="nav-link">Sign In</a> </li> {% endif %} </ul> </div> </div> </nav> <main role="main" class="container"> {% if errors %} {% for error in errors %} <div class="alert alert-danger" role="alert"> <p class="mb-3">{{ error.message }}</p> {% if error.debug %} <pre class="alert-pre border bg-light p-2"><code>{{ error.debug }}</code></pre> {% endif %} </div> {% endfor %} {% endif %} {% block content %}{% endblock %} </main> <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ho+j7jyWK8fNQe+A12Hb8AhRq26LrZ/JpcUGGOn+Y7RsweNrtN/tE3MoK7ZeZDyx" crossorigin="anonymous"></script> </body> </html>This code adds Bootstrap for simple styling, and Fabric Core for some simple icons. It also defines a global layout with a nav bar.
Create a new directory in the ./tutorial directory named
static.In the ./tutorial/static directory, create a new directory named
tutorial.In the ./tutorial/static/tutorial directory, create a new file named
app.css. Add the following code in that file.body { padding-top: 4.5rem; } .alert-pre { word-wrap: break-word; word-break: break-all; white-space: pre-wrap; } .external-link { padding-top: 6px; } .avatar-link { padding-top: 4px; padding-bottom: 4px; }Create a template for the home page that uses the layout. Create a new file in the ./tutorial/templates/tutorial directory named
home.htmland add the following code.{% extends "tutorial/layout.html" %} {% block content %} <div class="jumbotron"> <h1>Python Graph Tutorial</h1> <p class="lead">This sample app shows how to use the Microsoft Graph API to access a user's data from Python</p> {% if user.is_authenticated %} <h4>Welcome {{ user.name }}!</h4> <p>Use the navigation bar at the top of the page to get started.</p> {% else %} <a href="{% url 'signin' %}" class="btn btn-primary btn-large">Click here to sign in</a> {% endif %} </div> {% endblock %}Open the
./tutorial/views.pyfile and add the following new function.def initialize_context(request): context = {} # Check for any errors in the session error = request.session.pop('flash_error', None) if error != None: context['errors'] = [] context['errors'].append(error) # Check for user in the session context['user'] = request.session.get('user', {'is_authenticated': False}) return contextReplace the existing
homeview with the following.def home(request): context = initialize_context(request) return render(request, 'tutorial/home.html', context)Add a PNG file named no-profile-photo.png in the ./tutorial/static/tutorial directory.
Save all of your changes and restart the server. Now, the app should look very different.

Register the app in the portal
In this exercise, you will create a new Azure AD web application registration using the Azure Active Directory admin center.
Open a browser and navigate to the Azure Active Directory admin center. Login using a personal account (aka: Microsoft Account) or Work or School Account.
Select Azure Active Directory in the left-hand navigation, then select App registrations under Manage.

Select New registration. On the Register an application page, set the values as follows.
- Set Name to
Python Graph Tutorial. - Set Supported account types to Accounts in any organizational directory and personal Microsoft accounts.
- Under Redirect URI, set the first drop-down to
Weband set the value tohttp://localhost:8000/callback.

- Set Name to
Select Register. On the Python Graph Tutorial page, copy the value of the Application (client) ID and save it, you will need it in the next step.

Select Certificates & secrets under Manage. Select the New client secret button. Enter a value in Description and select one of the options for Expires and select Add.

Copy the client secret value before you leave this page. You will need it in the next step.
Important
This client secret is never shown again, so make sure you copy it now.

Add Azure AD authentication
In this exercise you will extend the application from the previous exercise to support authentication with Azure AD. This is required to obtain the necessary OAuth access token to call the Microsoft Graph. In this step you will integrate the MSAL for Python library into the application.
Create a new file in the root of the project named
oauth_settings.yml, and add the following content.app_id: "YOUR_APP_ID_HERE" app_secret: "YOUR_APP_SECRET_HERE" redirect: "http://localhost:8000/callback" scopes: - user.read - mailboxsettings.read - calendars.readwrite authority: "https://login.microsoftonline.com/common"Replace
YOUR_APP_ID_HEREwith the application ID from the Application Registration Portal, and replaceYOUR_APP_SECRET_HEREwith the password you generated.
Important
If you're using source control such as git, now would be a good time to exclude the oauth_settings.yml file from source control to avoid inadvertently leaking your app ID and password.
Implement sign-in
Create a new file in the ./tutorial directory named
auth_helper.pyand add the following code.import yaml import msal import os import time # Load the oauth_settings.yml file stream = open('oauth_settings.yml', 'r') settings = yaml.load(stream, yaml.SafeLoader) def load_cache(request): # Check for a token cache in the session cache = msal.SerializableTokenCache() if request.session.get('token_cache'): cache.deserialize(request.session['token_cache']) return cache def save_cache(request, cache): # If cache has changed, persist back to session if cache.has_state_changed: request.session['token_cache'] = cache.serialize() def get_msal_app(cache=None): # Initialize the MSAL confidential client auth_app = msal.ConfidentialClientApplication( settings['app_id'], authority=settings['authority'], client_credential=settings['app_secret'], token_cache=cache) return auth_app # Method to generate a sign-in flow def get_sign_in_flow(): auth_app = get_msal_app() return auth_app.initiate_auth_code_flow( settings['scopes'], redirect_uri=settings['redirect']) # Method to exchange auth code for access token def get_token_from_code(request): cache = load_cache(request) auth_app = get_msal_app(cache) # Get the flow saved in session flow = request.session.pop('auth_flow', {}) result = auth_app.acquire_token_by_auth_code_flow(flow, request.GET) save_cache(request, cache) return resultThis file will hold all of your authentication-related methods. The
get_sign_in_flowgenerates an authorization URL, and theget_token_from_codemethod exchanges the authorization response for an access token.Add the following
importstatement to the top of ./tutorial/views.py.from tutorial.auth_helper import get_sign_in_flow, get_token_from_codeAdd a sign-in view in the ./tutorial/views.py file.
def sign_in(request): # Get the sign-in flow flow = get_sign_in_flow() # Save the expected flow so we can use it in the callback try: request.session['auth_flow'] = flow except Exception as e: print(e) # Redirect to the Azure sign-in page return HttpResponseRedirect(flow['auth_uri'])Add a callback view in the ./tutorial/views.py file.
def callback(request): # Make the token request result = get_token_from_code(request) # Temporary! Save the response in an error so it's displayed request.session['flash_error'] = { 'message': 'Token retrieved', 'debug': format(result) } return HttpResponseRedirect(reverse('home'))Consider what these views do:
The
signinaction generates the Azure AD signin URL, saves the flow generated by the OAuth client, then redirects the browser to the Azure AD signin page.The
callbackaction is where Azure redirects after the signin is complete. That action uses the saved flow and the query string sent by Azure to request an access token. It then redirects back to the home page with the response in the temporary error value. You'll use this to verify that our sign-in is working before moving on.
Open ./tutorial/urls.py and replace the existing
pathstatements forsigninwith the following.path('signin', views.sign_in, name='signin'),Add a new
pathfor thecallbackview.path('callback', views.callback, name='callback'),Start the server and browse to
https://localhost:8000. Click the sign-in button and you should be redirected tohttps://login.microsoftonline.com. Login with your Microsoft account and consent to the requested permissions. The browser redirects to the app, showing the response, including the access token.
Get user details
Create a new file in the ./tutorial directory named
graph_helper.pyand add the following code.import requests import json graph_url = 'https://graph.microsoft.com/v1.0' def get_user(token): # Send GET to /me user = requests.get( '{0}/me'.format(graph_url), headers={ 'Authorization': 'Bearer {0}'.format(token) }, params={ '$select': 'displayName,mail,mailboxSettings,userPrincipalName' }) # Return the JSON result return user.json()The
get_usermethod makes a GET request to the Microsoft Graph/meendpoint to get the user's profile, using the access token you acquired previously.Update the
callbackmethod in ./tutorial/views.py to get the user's profile from Microsoft Graph. Add the followingimportstatement to the top of the file.from tutorial.graph_helper import *Replace the
callbackmethod with the following code.def callback(request): # Make the token request result = get_token_from_code(request) #Get the user's profile user = get_user(result['access_token']) # Temporary! Save the response in an error so it's displayed request.session['flash_error'] = { 'message': 'Token retrieved', 'debug': 'User: {0}\nToken: {1}'.format(user, result) } return HttpResponseRedirect(reverse('home'))The new code calls the
get_usermethod to request the user's profile. It adds the user object to the temporary output for testing.Add the following new methods to ./tutorial/auth_helper.py.
def store_user(request, user): try: request.session['user'] = { 'is_authenticated': True, 'name': user['displayName'], 'email': user['mail'] if (user['mail'] != None) else user['userPrincipalName'], 'timeZone': user['mailboxSettings']['timeZone'] } except Exception as e: print(e) def get_token(request): cache = load_cache(request) auth_app = get_msal_app(cache) accounts = auth_app.get_accounts() if accounts: result = auth_app.acquire_token_silent( settings['scopes'], account=accounts[0]) save_cache(request, cache) return result['access_token'] def remove_user_and_token(request): if 'token_cache' in request.session: del request.session['token_cache'] if 'user' in request.session: del request.session['user']Update the
callbackfunction in ./tutorial/views.py to store the user in the session and redirect back to the main page. Replace thefrom tutorial.auth_helper import get_sign_in_flow, get_token_from_codeline with the following.from tutorial.auth_helper import get_sign_in_flow, get_token_from_code, store_user, remove_user_and_token, get_tokenReplace the
callbackmethod with the following.def callback(request): # Make the token request result = get_token_from_code(request) #Get the user's profile user = get_user(result['access_token']) # Store user store_user(request, user) return HttpResponseRedirect(reverse('home'))
Implement sign-out
Add a new
sign_outview in ./tutorial/views.py.def sign_out(request): # Clear out the user and token remove_user_and_token(request) return HttpResponseRedirect(reverse('home'))Open ./tutorial/urls.py and replace the existing
pathstatements forsignoutwith the following.path('signout', views.sign_out, name='signout'),Restart the server and go through the sign-in process. You should end up back on the home page, but the UI should change to indicate that you are signed-in.

Click the user avatar in the top right corner to access the Sign Out link. Clicking Sign Out resets the session and returns you to the home page.

Refreshing tokens
At this point your application has an access token, which is sent in the Authorization header of API calls. This is the token that allows the app to access the Microsoft Graph on the user's behalf.
However, this token is short-lived. The token expires an hour after it is issued. This is where the refresh token becomes useful. The refresh token allows the app to request a new access token without requiring the user to sign in again.
Because this sample uses MSAL, you do not have to write any specific code to refresh the token. MSAL's acquire_token_silent method handles refreshing the token if needed.
Get a calendar view
In this exercise you will incorporate the Microsoft Graph into the application.
Get calendar events from Outlook
Start by adding a method to ./tutorial/graph_helper.py to fetch a view of the calendar between two dates. Add the following method.
def get_calendar_events(token, start, end, timezone): # Set headers headers = { 'Authorization': 'Bearer {0}'.format(token), 'Prefer': 'outlook.timezone="{0}"'.format(timezone) } # Configure query parameters to # modify the results query_params = { 'startDateTime': start, 'endDateTime': end, '$select': 'subject,organizer,start,end', '$orderby': 'start/dateTime', '$top': '50' } # Send GET to /me/events events = requests.get('{0}/me/calendarview'.format(graph_url), headers=headers, params=query_params) # Return the JSON result return events.json()Consider what this code is doing.
- The URL that will be called is
/v1.0/me/calendarview.- The
Prefer: outlook.timezoneheader causes the start and end times in the results to be adjusted to the user's time zone. - The
startDateTimeandendDateTimeparameters set the start and end of the view. - The
$selectparameter limits the fields returned for each events to just those the view will actually use. - The
$orderbyparameter sorts the results by start time. - The
$topparameter limits the results to 50 events.
- The
- The URL that will be called is
Add the following code to ./tutorial/graph_helper.py to lookup an IANA time zone identifier based on a Windows time zone name. This is necessary because Microsoft Graph can return time zones as Windows time zone names, and the Python datetime library requires IANA time zone identifiers.
# Basic lookup for mapping Windows time zone identifiers to # IANA identifiers # Mappings taken from # https://github.com/unicode-org/cldr/blob/master/common/supplemental/windowsZones.xml zone_mappings = { 'Dateline Standard Time': 'Etc/GMT+12', 'UTC-11': 'Etc/GMT+11', 'Aleutian Standard Time': 'America/Adak', 'Hawaiian Standard Time': 'Pacific/Honolulu', 'Marquesas Standard Time': 'Pacific/Marquesas', 'Alaskan Standard Time': 'America/Anchorage', 'UTC-09': 'Etc/GMT+9', 'Pacific Standard Time (Mexico)': 'America/Tijuana', 'UTC-08': 'Etc/GMT+8', 'Pacific Standard Time': 'America/Los_Angeles', 'US Mountain Standard Time': 'America/Phoenix', 'Mountain Standard Time (Mexico)': 'America/Chihuahua', 'Mountain Standard Time': 'America/Denver', 'Central America Standard Time': 'America/Guatemala', 'Central Standard Time': 'America/Chicago', 'Easter Island Standard Time': 'Pacific/Easter', 'Central Standard Time (Mexico)': 'America/Mexico_City', 'Canada Central Standard Time': 'America/Regina', 'SA Pacific Standard Time': 'America/Bogota', 'Eastern Standard Time (Mexico)': 'America/Cancun', 'Eastern Standard Time': 'America/New_York', 'Haiti Standard Time': 'America/Port-au-Prince', 'Cuba Standard Time': 'America/Havana', 'US Eastern Standard Time': 'America/Indianapolis', 'Turks And Caicos Standard Time': 'America/Grand_Turk', 'Paraguay Standard Time': 'America/Asuncion', 'Atlantic Standard Time': 'America/Halifax', 'Venezuela Standard Time': 'America/Caracas', 'Central Brazilian Standard Time': 'America/Cuiaba', 'SA Western Standard Time': 'America/La_Paz', 'Pacific SA Standard Time': 'America/Santiago', 'Newfoundland Standard Time': 'America/St_Johns', 'Tocantins Standard Time': 'America/Araguaina', 'E. South America Standard Time': 'America/Sao_Paulo', 'SA Eastern Standard Time': 'America/Cayenne', 'Argentina Standard Time': 'America/Buenos_Aires', 'Greenland Standard Time': 'America/Godthab', 'Montevideo Standard Time': 'America/Montevideo', 'Magallanes Standard Time': 'America/Punta_Arenas', 'Saint Pierre Standard Time': 'America/Miquelon', 'Bahia Standard Time': 'America/Bahia', 'UTC-02': 'Etc/GMT+2', 'Azores Standard Time': 'Atlantic/Azores', 'Cape Verde Standard Time': 'Atlantic/Cape_Verde', 'UTC': 'Etc/GMT', 'GMT Standard Time': 'Europe/London', 'Greenwich Standard Time': 'Atlantic/Reykjavik', 'Sao Tome Standard Time': 'Africa/Sao_Tome', 'Morocco Standard Time': 'Africa/Casablanca', 'W. Europe Standard Time': 'Europe/Berlin', 'Central Europe Standard Time': 'Europe/Budapest', 'Romance Standard Time': 'Europe/Paris', 'Central European Standard Time': 'Europe/Warsaw', 'W. Central Africa Standard Time': 'Africa/Lagos', 'Jordan Standard Time': 'Asia/Amman', 'GTB Standard Time': 'Europe/Bucharest', 'Middle East Standard Time': 'Asia/Beirut', 'Egypt Standard Time': 'Africa/Cairo', 'E. Europe Standard Time': 'Europe/Chisinau', 'Syria Standard Time': 'Asia/Damascus', 'West Bank Standard Time': 'Asia/Hebron', 'South Africa Standard Time': 'Africa/Johannesburg', 'FLE Standard Time': 'Europe/Kiev', 'Israel Standard Time': 'Asia/Jerusalem', 'Kaliningrad Standard Time': 'Europe/Kaliningrad', 'Sudan Standard Time': 'Africa/Khartoum', 'Libya Standard Time': 'Africa/Tripoli', 'Namibia Standard Time': 'Africa/Windhoek', 'Arabic Standard Time': 'Asia/Baghdad', 'Turkey Standard Time': 'Europe/Istanbul', 'Arab Standard Time': 'Asia/Riyadh', 'Belarus Standard Time': 'Europe/Minsk', 'Russian Standard Time': 'Europe/Moscow', 'E. Africa Standard Time': 'Africa/Nairobi', 'Iran Standard Time': 'Asia/Tehran', 'Arabian Standard Time': 'Asia/Dubai', 'Astrakhan Standard Time': 'Europe/Astrakhan', 'Azerbaijan Standard Time': 'Asia/Baku', 'Russia Time Zone 3': 'Europe/Samara', 'Mauritius Standard Time': 'Indian/Mauritius', 'Saratov Standard Time': 'Europe/Saratov', 'Georgian Standard Time': 'Asia/Tbilisi', 'Volgograd Standard Time': 'Europe/Volgograd', 'Caucasus Standard Time': 'Asia/Yerevan', 'Afghanistan Standard Time': 'Asia/Kabul', 'West Asia Standard Time': 'Asia/Tashkent', 'Ekaterinburg Standard Time': 'Asia/Yekaterinburg', 'Pakistan Standard Time': 'Asia/Karachi', 'Qyzylorda Standard Time': 'Asia/Qyzylorda', 'India Standard Time': 'Asia/Calcutta', 'Sri Lanka Standard Time': 'Asia/Colombo', 'Nepal Standard Time': 'Asia/Katmandu', 'Central Asia Standard Time': 'Asia/Almaty', 'Bangladesh Standard Time': 'Asia/Dhaka', 'Omsk Standard Time': 'Asia/Omsk', 'Myanmar Standard Time': 'Asia/Rangoon', 'SE Asia Standard Time': 'Asia/Bangkok', 'Altai Standard Time': 'Asia/Barnaul', 'W. Mongolia Standard Time': 'Asia/Hovd', 'North Asia Standard Time': 'Asia/Krasnoyarsk', 'N. Central Asia Standard Time': 'Asia/Novosibirsk', 'Tomsk Standard Time': 'Asia/Tomsk', 'China Standard Time': 'Asia/Shanghai', 'North Asia East Standard Time': 'Asia/Irkutsk', 'Singapore Standard Time': 'Asia/Singapore', 'W. Australia Standard Time': 'Australia/Perth', 'Taipei Standard Time': 'Asia/Taipei', 'Ulaanbaatar Standard Time': 'Asia/Ulaanbaatar', 'Aus Central W. Standard Time': 'Australia/Eucla', 'Transbaikal Standard Time': 'Asia/Chita', 'Tokyo Standard Time': 'Asia/Tokyo', 'North Korea Standard Time': 'Asia/Pyongyang', 'Korea Standard Time': 'Asia/Seoul', 'Yakutsk Standard Time': 'Asia/Yakutsk', 'Cen. Australia Standard Time': 'Australia/Adelaide', 'AUS Central Standard Time': 'Australia/Darwin', 'E. Australia Standard Time': 'Australia/Brisbane', 'AUS Eastern Standard Time': 'Australia/Sydney', 'West Pacific Standard Time': 'Pacific/Port_Moresby', 'Tasmania Standard Time': 'Australia/Hobart', 'Vladivostok Standard Time': 'Asia/Vladivostok', 'Lord Howe Standard Time': 'Australia/Lord_Howe', 'Bougainville Standard Time': 'Pacific/Bougainville', 'Russia Time Zone 10': 'Asia/Srednekolymsk', 'Magadan Standard Time': 'Asia/Magadan', 'Norfolk Standard Time': 'Pacific/Norfolk', 'Sakhalin Standard Time': 'Asia/Sakhalin', 'Central Pacific Standard Time': 'Pacific/Guadalcanal', 'Russia Time Zone 11': 'Asia/Kamchatka', 'New Zealand Standard Time': 'Pacific/Auckland', 'UTC+12': 'Etc/GMT-12', 'Fiji Standard Time': 'Pacific/Fiji', 'Chatham Islands Standard Time': 'Pacific/Chatham', 'UTC+13': 'Etc/GMT-13', 'Tonga Standard Time': 'Pacific/Tongatapu', 'Samoa Standard Time': 'Pacific/Apia', 'Line Islands Standard Time': 'Pacific/Kiritimati' } def get_iana_from_windows(windows_tz_name): if windows_tz_name in zone_mappings: return zone_mappings[windows_tz_name] else: # Assume if not found value is # already an IANA name return windows_tz_nameAdd the following view to ./tutorial/views.py.
def calendar(request): context = initialize_context(request) user = context['user'] # Load the user's time zone # Microsoft Graph can return the user's time zone as either # a Windows time zone name or an IANA time zone identifier # Python datetime requires IANA, so convert Windows to IANA time_zone = get_iana_from_windows(user['timeZone']) tz_info = tz.gettz(time_zone) # Get midnight today in user's time zone today = datetime.now(tz_info).replace( hour=0, minute=0, second=0, microsecond=0) # Based on today, get the start of the week (Sunday) if (today.weekday() != 6): start = today - timedelta(days=today.isoweekday()) else: start = today end = start + timedelta(days=7) token = get_token(request) events = get_calendar_events( token, start.isoformat(timespec='seconds'), end.isoformat(timespec='seconds'), user['timeZone']) context['errors'] = [ { 'message': 'Events', 'debug': format(events)} ] return render(request, 'tutorial/home.html', context)Open ./tutorial/urls.py and replace the existing
pathstatements forcalendarwith the following.path('calendar', views.calendar, name='calendar'),Sign in and click the Calendar link in the nav bar. If everything works, you should see a JSON dump of events on the user's calendar.
Display the results
Now you can add a template to display the results in a more user-friendly manner.
Create a new file in the ./tutorial/templates/tutorial directory named
calendar.htmland add the following code.{% extends "tutorial/layout.html" %} {% block content %} <h1>Calendar</h1> <a href="/calendar/new" class="btn btn-light btn-sm mb-3">New event</a> <table class="table"> <thead> <tr> <th scope="col">Organizer</th> <th scope="col">Subject</th> <th scope="col">Start</th> <th scope="col">End</th> </tr> </thead> <tbody> {% if events %} {% for event in events %} <tr> <td>{{ event.organizer.emailAddress.name }}</td> <td>{{ event.subject }}</td> <td>{{ event.start.dateTime|date:'n/d/Y g:i A' }}</td> <td>{{ event.end.dateTime|date:'n/d/Y g:i A' }}</td> </tr> {% endfor %} {% endif %} </tbody> </table> {% endblock %}That will loop through a collection of events and add a table row for each one.
Replace the
calendarview in ./tutorial/views.py with the following code.def calendar(request): context = initialize_context(request) user = context['user'] # Load the user's time zone # Microsoft Graph can return the user's time zone as either # a Windows time zone name or an IANA time zone identifier # Python datetime requires IANA, so convert Windows to IANA time_zone = get_iana_from_windows(user['timeZone']) tz_info = tz.gettz(time_zone) # Get midnight today in user's time zone today = datetime.now(tz_info).replace( hour=0, minute=0, second=0, microsecond=0) # Based on today, get the start of the week (Sunday) if (today.weekday() != 6): start = today - timedelta(days=today.isoweekday()) else: start = today end = start + timedelta(days=7) token = get_token(request) events = get_calendar_events( token, start.isoformat(timespec='seconds'), end.isoformat(timespec='seconds'), user['timeZone']) if events: # Convert the ISO 8601 date times to a datetime object # This allows the Django template to format the value nicely for event in events['value']: event['start']['dateTime'] = parser.parse(event['start']['dateTime']) event['end']['dateTime'] = parser.parse(event['end']['dateTime']) context['events'] = events['value'] return render(request, 'tutorial/calendar.html', context)Refresh the page and the app should now render a table of events.

Create a new event
In this section you will add the ability to create events on the user's calendar.
Add the following method to ./tutorial/graph_helper.py to create a new event.
def create_event(token, subject, start, end, attendees=None, body=None, timezone='UTC'): # Create an event object # https://docs.microsoft.com/graph/api/resources/event?view=graph-rest-1.0 new_event = { 'subject': subject, 'start': { 'dateTime': start, 'timeZone': timezone }, 'end': { 'dateTime': end, 'timeZone': timezone } } if attendees: attendee_list = [] for email in attendees: # Create an attendee object # https://docs.microsoft.com/graph/api/resources/attendee?view=graph-rest-1.0 attendee_list.append({ 'type': 'required', 'emailAddress': { 'address': email } }) new_event['attendees'] = attendee_list if body: # Create an itemBody object # https://docs.microsoft.com/graph/api/resources/itembody?view=graph-rest-1.0 new_event['body'] = { 'contentType': 'text', 'content': body } # Set headers headers = { 'Authorization': 'Bearer {0}'.format(token), 'Content-Type': 'application/json' } requests.post('{0}/me/events'.format(graph_url), headers=headers, data=json.dumps(new_event))
Create a new event form
Create a new file in the ./tutorial/templates/tutorial directory named
newevent.htmland add the following code.{% extends "tutorial/layout.html" %} {% block content %} <h1>New event</h1> <form method="POST"> {% csrf_token %} <div class="form-group"> <label>Subject</label> <input class="form-control" name="ev-subject" type="text"> </div> <div class="form-group"> <label>Attendees</label> <input class="form-control" name="ev-attendees" type="text" placeholder="Separate multiple email addresses with a semicolon (';')"> </div> <div class="form-row"> <div class="col"> <div class="form-group"> <label>Start</label> <input class="form-control" name="ev-start" type="datetime-local"> </div> </div> <div class="col"> <div class="form-group"> <label>End</label> <input class="form-control" name="ev-end" type="datetime-local"> </div> </div> </div> <div class="form-group mb-3"> <label>Body</label> <textarea class="form-control" name="ev-body" rows="3"></textarea> </div> <input class="btn btn-primary mr-2" type="submit" value="Create" /> <a class="btn btn-secondary" href="/calendar">Cancel</a> </form> {% endblock %}Add the following view to ./tutorial/views.py.
def newevent(request): context = initialize_context(request) user = context['user'] if request.method == 'POST': # Validate the form values # Required values if (not request.POST['ev-subject']) or \ (not request.POST['ev-start']) or \ (not request.POST['ev-end']): context['errors'] = [ { 'message': 'Invalid values', 'debug': 'The subject, start, and end fields are required.'} ] return render(request, 'tutorial/newevent.html', context) attendees = None if request.POST['ev-attendees']: attendees = request.POST['ev-attendees'].split(';') body = request.POST['ev-body'] # Create the event token = get_token(request) create_event( token, request.POST['ev-subject'], request.POST['ev-start'], request.POST['ev-end'], attendees, request.POST['ev-body'], user['timeZone']) # Redirect back to calendar view return HttpResponseRedirect(reverse('calendar')) else: # Render the form return render(request, 'tutorial/newevent.html', context) print('hello')Open ./tutorial/urls.py and add a
pathstatements for theneweventview.path('calendar/new', views.newevent, name='newevent'),Save your changes and refresh the app. On the Calendar page, select the New event button. Fill in the form and select Create to create the event.

Congratulations!
You've completed the Python Microsoft Graph tutorial. Now that you have a working app that calls Microsoft Graph, you can experiment and add new features. Visit the Overview of Microsoft Graph to see all of the data you can access with Microsoft Graph.
Feedback
Please provide any feedback on this tutorial in the GitHub repository.
Have an issue with this section? If so, please give us some feedback so we can improve this section.