Microsoft Graph チュートリアル Pythonをやーる（Python 3.7.0、Django 2.1.7）


[Build Python Django apps with Microsoft Graph](https://docs.microsoft.com/ja-jp/graph/tutorials/python)をやってみました。

## システム環境
- Windows 10
- Anaconda 
- Python 3.7.0
- Django 2.1.7

## Django Webアプリを作成

~~~
$ pip install Django
$ D:\Anaconda3\envs\py37\Scripts\django-admin startproject graph_tutorial
$ cd graph_tutorial
$ python manage.py runserver
~~~

`http://localhost:8000/`にアクセスし、下記が見えたらOKです。
<img width="767" alt="graph1.PNG" src="https://qiita-image-store.s3.amazonaws.com/0/63863/2fac2a03-e0ee-5f60-ab9b-00ed51efee68.png">

~~~
$ python manage.py startapp tutorial
~~~

`./graph_tutorial/settings.py`を開き、`INSTALLED_APPS`に`tutorial`を加えます。

~~~python
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'tutorial',
]
~~~

データベースを初期化します。

~~~
$ python manage.py migrate
~~~

`./tutorial/urls.py`を作成します。

~~~python
from django.urls import path

from . import views

urlpatterns = [
  # /tutorial
  path('', views.home, name='home'),
]
~~~

`./graph_tutorial/urls.py`を編集します。

~~~python
from django.contrib import admin
from django.urls import path, include
from tutorial import views

urlpatterns = [
    path('tutorial/', include('tutorial.urls')),
    path('admin/', admin.site.urls),
]
~~~

`./tutorial/views.py`を編集します。

~~~python
from django.shortcuts import render
from django.http import HttpResponse, HttpResponseRedirect

def home(request):
  # Temporary!
  return HttpResponse("Welcome to the tutorial.")
~~~

下記をインストールします。

~~~
$ pip install requests_oauthlib
$ pip install pyyaml
$ pip install python-dateutil
~~~

`./tutorial/templates/tutorial/layout.html`を作成します。

~~~html
<!DOCTYPE html>
<html>
  <head>
    <title>Python Graph Tutorial</title>

    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css"
      integrity="sha384-WskhaSGFgHYWDcbwN70/dfYBj47jz9qbsMId/iRN3ewGhXQFZCSftd1LZCfmhktB" crossorigin="anonymous">
    <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.1.0/css/all.css"
      integrity="sha384-lKuwvrZot6UHsBSfcMvOkWwlCMgc0TaWr+30HWe3a4ltaBwTZhyTEggF5tJv8tbt" crossorigin="anonymous">
    {% load static %}
    <link rel="stylesheet" href="{% static "tutorial/app.css" %}">
    <script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js"
      integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" crossorigin="anonymous"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/js/bootstrap.min.js"
      integrity="sha384-smHYKdLADwkXOn1EmN1qk/HfnUcbVRZyYmZ4qpPea6sjB/pTJ0euyQp0Mk8ck+5T" crossorigin="anonymous"></script>
  </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="#">Calendar</a>
              </li>
            {% endif %}
          </ul>
          <ul class="navbar-nav justify-content-end">
            <li class="nav-item">
              <a class="nav-link" href="https://developer.microsoft.com/graph/docs/concepts/overview" target="_blank">
                <i class="fas fa-external-link-alt mr-1"></i>Docs
              </a>
            </li>
            {% if user.is_authenticated %}
              <li class="nav-item dropdown">
                <a class="nav-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 %}
                    <i class="far fa-user-circle fa-lg rounded-circle align-self-center mr-2" style="width: 32px;"></i>
                  {% 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="#" class="dropdown-item">Sign Out</a>
                </div>
              </li>
            {% else %}
              <li class="nav-item">
                <a href="#" 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>
  </body>
</html>
~~~

`./tutorial/static/tutorial/app.css`を作成します。

~~~css
body {
  padding-top: 4.5rem;
}

.alert-pre {
  word-wrap: break-word;
  word-break: break-all;
  white-space: pre-wrap;
}
~~~

`./tutorial/templates/tutorial/home.html`を作成します。

~~~html
{% 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 Outlook and OneDrive 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="#" class="btn btn-primary btn-large">Click here to sign in</a>
  {% endif %}
</div>
{% endblock %}
~~~

`./tutorial/views.py`を編集します。

~~~python
from django.shortcuts import render
from django.http import HttpResponse, HttpResponseRedirect

def home(request):
  context = initialize_context(request)

  return render(request, 'tutorial/home.html', context)

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 context
~~~

実行します。（新しくコマンドプロンプトを開いて実行しておけばOK）

~~~
$ python manage.py runserver
~~~

`http://localhost:8000/tutorial/`にアクセスし、下記が見えたらOKです。

<img width="850" alt="graph2.PNG" src="https://qiita-image-store.s3.amazonaws.com/0/63863/c5576bb9-ed4c-0dd1-5ff1-e8da141668a8.png">

## アプリを登録
[Application Registration Portal](https://apps.dev.microsoft.com/)でアプリを登録します。

`Python Graph Tutorial`という名前でアプリケーションの作成をクリックします。

<img width="592" alt="graph3.PNG" src="https://qiita-image-store.s3.amazonaws.com/0/63863/baa07280-da16-70dc-85f6-e9ad9fe96946.png">

新しいパスワードを生成し、メモっておいてください。

![graph4.PNG](https://qiita-image-store.s3.amazonaws.com/0/63863/26280d20-f281-a061-d430-55d399b3669d.png)

`プラットフォームの追加`をクリックし、
![graph5.PNG](https://qiita-image-store.s3.amazonaws.com/0/63863/9ef7c923-4a60-0dc3-03d5-ed1d1fb52c35.png)

`Web`を選択、
<img width="594" alt="graph6.PNG" src="https://qiita-image-store.s3.amazonaws.com/0/63863/f5c91703-007f-74bf-d79a-d9b1b588e762.png">

`リダイレクトURL`を`http://localhost:8000/tutorial/callback`に設定します。
<img width="882" alt="graph7.PNG" src="https://qiita-image-store.s3.amazonaws.com/0/63863/553aff1c-79cc-9bcd-f67f-7e0c7610c226.png">

保存をクリックすれば完了です。

## Azure AD authenticationの追加

Djangoアプリのrootに`oauth_settings.yml`を作成します。

~~~yml
app_id: YOUR_APP_ID_HERE
app_secret: YOUR_APP_PASSWORD_HERE
redirect: http://localhost:8000/tutorial/callback
scopes: openid profile offline_access user.read calendars.read
authority: https://login.microsoftonline.com/common
authorize_endpoint: /oauth2/v2.0/authorize
token_endpoint: /oauth2/v2.0/token
~~~

`YOUR_APP_ID_HERE`、`YOUR_APP_PASSWORD_HERE`には先ほど登録したものを入れてください。

`./tutorial/auth_helper.py`を作成します。

~~~python
import yaml
from requests_oauthlib import OAuth2Session
import os
import time

# This is necessary for testing with non-HTTPS localhost
# Remove this if deploying to production
os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = '1'

# This is necessary because Azure does not guarantee
# to return scopes in the same case and order as requested
os.environ['OAUTHLIB_RELAX_TOKEN_SCOPE'] = '1'
os.environ['OAUTHLIB_IGNORE_SCOPE_CHANGE'] = '1'

# Load the oauth_settings.yml file
stream = open('oauth_settings.yml', 'r')
settings = yaml.load(stream)
authorize_url = '{0}{1}'.format(settings['authority'], settings['authorize_endpoint'])
token_url = '{0}{1}'.format(settings['authority'], settings['token_endpoint'])

# Method to generate a sign-in url
def get_sign_in_url():
  # Initialize the OAuth client
  aad_auth = OAuth2Session(settings['app_id'],
    scope=settings['scopes'],
    redirect_uri=settings['redirect'])

  sign_in_url, state = aad_auth.authorization_url(authorize_url, prompt='login')

  return sign_in_url, state

# Method to exchange auth code for access token
def get_token_from_code(callback_url, expected_state):
  # Initialize the OAuth client
  aad_auth = OAuth2Session(settings['app_id'],
    state=expected_state,
    scope=settings['scopes'],
    redirect_uri=settings['redirect'])

  token = aad_auth.fetch_token(token_url,
    client_secret = settings['app_secret'],
    authorization_response=callback_url)

  return token
~~~

`./tutorial/views.py`に下記を追記します。

~~~python
from django.urls import reverse
from tutorial.auth_helper import get_sign_in_url, get_token_from_code

def sign_in(request):
  # Get the sign-in URL
  sign_in_url, state = get_sign_in_url()
  # Save the expected state so we can validate in the callback
  request.session['auth_state'] = state
  # Redirect to the Azure sign-in page
  return HttpResponseRedirect(sign_in_url)

def callback(request):
  # Get the state saved in session
  expected_state = request.session.pop('auth_state', '')
  # Make the token request
  token = get_token_from_code(request.get_full_path(), expected_state)
  # Temporary! Save the response in an error so it's displayed
  request.session['flash_error'] = { 'message': 'Token retrieved', 'debug': format(token) }
  return HttpResponseRedirect(reverse('home'))
~~~

`./tutorial/urls.py`の`urlpatterns`を編集します。

~~~python
urlpatterns = [
  # /tutorial
  path('', views.home, name='home'),
  path('signin', views.sign_in, name='signin'),
  path('callback', views.callback, name='callback'),
]
~~~

`./tutorial/templates/tutorial/home.html`の10行目の

~~~html
<a href="{% url 'signin' %}" class="btn btn-primary btn-large">Click here to sign in</a>
~~~

を次のように書き換えます。

~~~html
<a href="{% url 'signin' %}" class="nav-link">Sign In</a>
~~~

実行して、`http://localhost:8000/tutorial/`にアクセス、Sign InするとMicrosoftアカウントでサインイン、アプリの許可をします。（チュートリアルはhttpsになっており、アプリのリダイレクトURLの変更やDjangoアプリのhttps化が必要と思われる）

許可すると、この時点ではTokenの情報が表示されます。

`./tutorial/graph_helper.py`を作成します。

~~~python
from requests_oauthlib import OAuth2Session

graph_url = 'https://graph.microsoft.com/v1.0'

def get_user(token):
  graph_client = OAuth2Session(token=token)
  # Send GET to /me
  user = graph_client.get('{0}/me'.format(graph_url))
  # Return the JSON result
  return user.json()
~~~

`./tutorial/views.py`の`callback`を編集します。

~~~python
from tutorial.graph_helper import get_user

def callback(request):
  # Get the state saved in session
  expected_state = request.session.pop('auth_state', '')
  # Make the token request
  token = get_token_from_code(request.get_full_path(), expected_state)

  # Get the user's profile
  user = get_user(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, token) }
  return HttpResponseRedirect(reverse('home'))
~~~


`./tutorial/auth_helper.py`に下記を追記します。

~~~python
def store_token(request, token):
  request.session['oauth_token'] = token

def store_user(request, user):
  request.session['user'] = {
    'is_authenticated': True,
    'name': user['displayName'],
    'email': user['mail'] if (user['mail'] != None) else user['userPrincipalName']
  }

def get_token(request):
  token = request.session['oauth_token']
  return token

def remove_user_and_token(request):
  if 'oauth_token' in request.session:
    del request.session['oauth_token']

  if 'user' in request.session:
    del request.session['user']
~~~

`./tutorial/views.py`の`from tutorial.auth_helper import get_sign_in_url, get_token_from_code`を下記に書き換えます。

~~~python
from tutorial.auth_helper import get_sign_in_url, get_token_from_code, store_token, store_user, remove_user_and_token, get_token
~~~

`./tutorial/views.py`の`callback`も編集します。

~~~python
def callback(request):
  # Get the state saved in session
  expected_state = request.session.pop('auth_state', '')
  # Make the token request
  token = get_token_from_code(request.get_full_path(), expected_state)

  # Get the user's profile
  user = get_user(token)

  # Save token and user
  store_token(request, token)
  store_user(request, user)

  return HttpResponseRedirect(reverse('home'))
~~~

`./tutorial/views.py`に下記を追記します。

~~~python
def sign_out(request):
  # Clear out the user and token
  remove_user_and_token(request)

  return HttpResponseRedirect(reverse('home'))
~~~

`./tutorial/urls.py`の`urlpatterns`に下記を追記します。

~~~python
path('signout', views.sign_out, name='signout'),
~~~

`./tutorial/templates/tutorial/layout.html`の57行目の

~~~html
<a href="#" class="dropdown-item">Sign Out</a> 
~~~

を下記に書き換えます。

~~~html
<a href="{% url 'signout' %}" class="dropdown-item">Sign Out</a>
~~~

実行して、サインインしてみます。名前が表示されていれば、OKです。

![graph14.PNG](https://qiita-image-store.s3.amazonaws.com/0/63863/3154fed0-ff76-880f-a91e-1eb8fba2a64d.png)

`./tutorial/auth_helper.py`の`get_token`を編集します。

~~~python
def get_token(request):
  token = request.session['oauth_token']
  if token != None:
    # Check expiration
    now = time.time()
    # Subtract 5 minutes from expiration to account for clock skew
    expire_time = token['expires_at'] - 300
    if now >= expire_time:
      # Refresh the token
      aad_auth = OAuth2Session(settings['app_id'],
        token = token,
        scope=settings['scopes'],
        redirect_uri=settings['redirect'])

      refresh_params = {
        'client_id': settings['app_id'],
        'client_secret': settings['app_secret'],
      }
      new_token = aad_auth.refresh_token(token_url, **refresh_params)

      # Save new token
      store_token(request, new_token)

      # Return new access token
      return new_token

    else:
      # Token still valid, just return it
      return token
~~~

tokenの有効期限が切れると再度ログインになります。

## カレンダーのデータを取得

`./tutorial/graph_helper.py`に下記を追記します。

~~~
def get_calendar_events(token):
  graph_client = OAuth2Session(token=token)

  # Configure query parameters to
  # modify the results
  query_params = {
    '$select': 'subject,organizer,start,end',
    '$orderby': 'createdDateTime DESC'
  }

  # Send GET to /me/events
  events = graph_client.get('{0}/me/events'.format(graph_url), params=query_params)
  # Return the JSON result
  return events.json()
~~~

`./tutorial/views.py`の`from tutorial.graph_helper import get_user`を下記に書き換えます。

~~~python
from tutorial.graph_helper import get_user, get_calendar_eventsを
~~~

`./tutorial/views.py`に下記を追記します。

~~~python
def calendar(request):
  context = initialize_context(request)

  token = get_token(request)

  events = get_calendar_events(token)

  context['errors'] = [
    { 'message': 'Events', 'debug': format(events)}
  ]

  return render(request, 'tutorial/home.html', context)
~~~

`./tutorial/urls.py`の`urlpatterns`に下記を追記します。

~~~python
path('calendar', views.calendar, name='calendar'),
~~~

`./tutorial/templates/tutorial/layout.html`の34行目、

~~~html
<a class="nav-link{% if request.resolver_match.view_name == 'calendar' %} active{% endif %}" href="#">Calendar</a>
~~~

を下記に書き換えます。

~~~html
<a class="nav-link{% if request.resolver_match.view_name == 'calendar' %} active{% endif %}" href="{% url 'calendar' %}">Calendar</a>
~~~


`./tutorial/templates/tutorial/calendar.html`を作成します。

~~~html
{% extends "tutorial/layout.html" %}
{% block content %}
<h1>Calendar</h1>
<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:'SHORT_DATETIME_FORMAT' }}</td>
          <td>{{ event.end.dateTime|date:'SHORT_DATETIME_FORMAT' }}</td>
        </tr>
      {% endfor %}
    {% endif %}
  </tbody>
</table>
{% endblock %}
~~~

実行するとナビゲーションにCalenderが表示され、クリックするとJSON形式のカレンダー情報が表示されます。

`./tutorial/views.py`に下記を追記します。

~~~python
import dateutil.parser
~~~

`./tutorial/views.py`の`calendar`を編集します。

~~~python
def calendar(request):
  context = initialize_context(request)

  token = get_token(request)

  events = get_calendar_events(token)

  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'] = dateutil.parser.parse(event['start']['dateTime'])
      event['end']['dateTime'] = dateutil.parser.parse(event['end']['dateTime'])

    context['events'] = events['value']

  return render(request, 'tutorial/calendar.html', context)
~~~

実行して、ナビゲーションのCalenderをクリックし、カレンダーが表示されたらOKです。

![graph16.PNG](https://qiita-image-store.s3.amazonaws.com/0/63863/69ed17e0-27ee-d37f-6d49-a13ca0b97213.png)


## まとめ
- Microsoft Graphを用いて、ユーザー名やカレンダーの情報を取得できました
- Microsoft Teamsと連携していきたいです

<!--
Djangoのバージョン確認方法

~~~
$ python
>>> import django
>>> django.get_version()
'2.1.7'
~~~
-->