使用 Microsoft Graph 生成 Ruby on Rails 应用
本教程指导你如何生成 Ruby on Rails Web 应用,该应用使用 Microsoft Graph API 检索用户的日历信息。
先决条件
在开始本教程之前,应在开发计算机上安装以下工具。
您还应该有一个在 Outlook.com 上拥有邮箱的个人 Microsoft 帐户,或者一个 Microsoft 工作或学校帐户。 如果你没有 Microsoft 帐户,则有几个选项可以获取免费帐户:
- 你可以 注册新的个人 Microsoft 帐户。
- 你可以注册开发人员计划Microsoft 365免费订阅Microsoft 365订阅。
备注
本教程是使用所需工具的以下版本编写的。 本指南中的步骤可能与其他版本一起运行,但该版本尚未经过测试。
- Ruby 版本 3.0.1
- SQLite3 版本 3.35.5
- Node.js版本 14.15.0
- 版本 1.22.0
反馈
Please provide any feedback on this tutorial in the GitHub repository.
创建 Ruby on Rails Web 应用
在此练习中,你将使用 Ruby on Rails 生成 Web 应用。
如果尚未安装 Rails,可以通过以下命令从命令行界面 (CLI) 安装它。
gem install rails -v 6.1.3.1
打开 CLI,导航到你拥有创建文件权限的目录,然后运行以下命令以创建新的 Rails 应用。
rails new graph-tutorial
导航到此新目录并输入以下命令以启动本地 Web 服务器。
rails server
打开浏览器,并导航到
http://localhost:3000
。 如果一切正常,你将看到"Yay! 你使用 Rails!" 消息。 如果你看不到该消息,请查看 Rails 入门指南。
安装 gem
在继续之前,请安装一些稍后将使用的其他 gem:
- omniauth-oauth2, 用于处理登录和 OAuth 令牌流。
- omniauth-rails_csrf_protection 向 OmniAuth 添加 CSRF 保护。
- 用于调用Microsoft Graph。
- activerecord-session_store 用于存储数据库中的会话。
打开 ./Gemfile 并添加以下行。
# OAuth gem 'omniauth-oauth2', '~> 1.7.1' # OmniAuth CSRF protection gem 'omniauth-rails_csrf_protection', '~> 1.0.0' # REST calls to Microsoft Graph gem 'httparty', '~> 0.18.1' # Session storage in database gem 'activerecord-session_store', '~> 2.0.0'
在 CLI 中,运行以下命令。
bundle install
在 CLI 中,运行以下命令以配置数据库以存储会话。
rails generate active_record:session_migration rake db:migrate
在
session_store.rb
./config/initializers 目录中创建名为 的新文件,并添加以下代码。Rails.application.config.session_store :active_record_store, :key => '_graph_app_session'
设计应用
在此部分中,你将为应用创建基本 UI。
打开 ./app/views/layouts/application.html.erb, 并将其内容替换为以下内容。
<!DOCTYPE html> <html> <head> <title>Ruby Graph Tutorial</title> <%= csrf_meta_tags %> <%= csp_meta_tag %> <link rel="shortcut icon" href="favicon.png"/> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.0/dist/css/bootstrap.min.css" integrity="sha384-B0vP5xmATw1+K9KRQjQERJvTumQW0nPEzvF6L/Z6nronJ3oUOFUFpCjEUQouq2+l" crossorigin="anonymous"> <link rel="stylesheet" href="https://static2.sharepointonline.com/files/fabric/office-ui-fabric-core/11.0.0/css/fabric.min.css"/> <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %> </head> <body> <nav class="navbar navbar-expand-md navbar-dark fixed-top bg-dark"> <div class="container"> <%= link_to "Ruby Graph Tutorial", root_path, class: "navbar-brand" %> <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"> <%= link_to "Home", root_path, class: "nav-link#{' active' if controller.controller_name == 'home'}" %> </li> <% if @user_name %> <li class="nav-item" data-turbolinks="false"> <%= link_to "Calendar", "/calendar", class: "nav-link#{' active' if controller.controller_name == 'calendar'}" %> </li> <% end %> </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"></i>Docs </a> </li> <% if @user_name %> <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 profile-photo"> <% else %> <%= image_tag "no-profile-photo.png", class: "rounded-circle align-self-center mr-2 profile-photo" %> <% end %> </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> <%= link_to "Sign Out", {:controller => :auth, :action => :signout}, :class => "dropdown-item" %> </div> </li> <% else %> <li class="nav-item"> <%= link_to "Sign In", "/auth/microsoft_graph_auth", method: :post, class: "nav-link" %> </li> <% end %> </ul> </div> </div> </nav> <main role="main" class="container"> <% if @errors %> <% @errors.each do |error| %> <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> <% end %> </div> <% end %> <% end %> <%= yield %> </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> <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %> </body> </html>
此代码为简单样式设置添加 Bootstrap,为一些简单图标添加Fabric Core。 它还定义具有导航栏的全局布局。
打开 ./app/assets/stylesheets/application.css, 将以下内容添加到文件的末尾。
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; } .profile-photo { width: 32px; }
使用下面的命令生成主页控制器。
rails generate controller Home index
将
index
控制器上的Home
操作配置为应用的默认页面。 打开 ./config/routes.rb, 并将其内容替换为以下内容Rails.application.routes.draw do get 'home/index' root 'home#index' # Add future routes here end
打开 ./app/view/home/index.html.erb, 并将其内容替换为以下内容。
<div class="jumbotron"> <h1>Ruby Graph Tutorial</h1> <p class="lead">This sample app shows how to use the Microsoft Graph API to access a user's data from Ruby</p> <% if @user_name %> <h4>Welcome <%= @user_name %>!</h4> <p>Use the navigation bar at the top of the page to get started.</p> <% else %> <%= link_to "Click here to sign in", "/auth/microsoft_graph_auth", method: :post, class: "btn btn-primary btn-large" %> <% end %> </div>
在 ./app/assets/images no-profile-photo.png 名为no-profile-photo.pngPNG 文件。
保存全部更改,重新启动服务器。 现在,应用看起来应该非常不同。
在门户中注册该应用
在此练习中,你将使用管理中心创建新的 Azure AD Azure Active Directory注册。
打开浏览器,并转到 Azure Active Directory 管理中心。 使用 个人帐户(亦称为“Microsoft 帐户”)或 工作或学校帐户 登录。
选择左侧导航栏中的“Azure Active Directory”,再选择“管理”下的“应用注册”。
选择“新注册”。 在“注册应用”页上,按如下方式设置值。
- 将“名称”设置为“
Ruby Graph Tutorial
”。 - 将“受支持的帐户类型”设置为“任何组织目录中的帐户和个人 Microsoft 帐户”。
- 在“重定向 URI”下,将第一个下拉列表设置为“
Web
”,并将值设置为“http://localhost:3000/auth/microsoft_graph_auth/callback
”。
- 将“名称”设置为“
选择“注册”。 在 Ruby Graph 教程 页面上,复制 Application (client) ID 的值并将其保存,你将在下一步中需要该值。
选择“管理”下的“证书和密码”。 选择“新客户端密码”按钮。 在“说明”中输入值,并选择“过期”下的一个选项,再选择“添加”。
离开此页前,先复制客户端密码值。 将在下一步中用到它。
重要
此客户端密码不会再次显示,所以请务必现在就复制它。
添加 Azure AD 身份验证
在此练习中,你将从上一练习中扩展应用程序,以支持使用 Azure AD 进行身份验证。 这是必需的,才能获取必要的 OAuth 访问令牌来调用 Microsoft Graph。 在此步骤中,你将 omniauth-oauth2 gem 集成到应用程序中,并创建自定义 OmniAuth 策略。
创建单独的文件来保存应用 ID 和密码。 在
oauth_environment_variables.rb
./config 文件夹中创建名为 的新文件,并添加以下代码。ENV['AZURE_APP_ID'] = 'YOUR_APP_ID_HERE' ENV['AZURE_APP_SECRET'] = 'YOUR_APP_SECRET_HERE' ENV['AZURE_SCOPES'] = 'openid profile email offline_access user.read mailboxsettings.read calendars.readwrite'
将
YOUR_APP_ID_HERE
替换为应用程序注册门户中的应用程序 ID,并替换为YOUR_APP_SECRET_HERE
生成的密码。重要
如果你使用的是源代码管理(如 git),那么现在应该将文件从源代码管理中排除,以避免意外泄露应用
oauth_environment_variables.rb
ID 和密码。打开 ./config/environment.rb, 在行前添加以下
Rails.application.initialize!
代码。# Load the Rails application. require_relative "application" # Load OAuth settings oauth_environment_variables = File.join(Rails.root, 'config', 'oauth_environment_variables.rb') load(oauth_environment_variables) if File.exist?(oauth_environment_variables) # Initialize the Rails application. Rails.application.initialize!
设置 OmniAuth
你已安装了 gem,但为了使它与 Azure OAuth 终结点一起运行,你需要 omniauth-oauth2
创建 OAuth2 策略。 这是一个 Ruby 类,用于定义向 Azure 提供程序提出 OAuth 请求的参数。
在
microsoft_graph_auth.rb
./lib'** 文件夹中创建名为 的新文件,并添加以下代码。require 'omniauth-oauth2' module OmniAuth module Strategies # Implements an OmniAuth strategy to get a Microsoft Graph # compatible token from Azure AD class MicrosoftGraphAuth < OmniAuth::Strategies::OAuth2 option :name, :microsoft_graph_auth DEFAULT_SCOPE = 'openid email profile User.Read'.freeze # Configure the Microsoft identity platform endpoints option :client_options, :site => 'https://login.microsoftonline.com', :authorize_url => '/common/oauth2/v2.0/authorize', :token_url => '/common/oauth2/v2.0/token' # Send the scope parameter during authorize option :authorize_options, [:scope] # Unique ID for the user is the id field uid { raw_info['id'] } # Get additional information after token is retrieved extra do { 'raw_info' => raw_info } end def raw_info # Get user profile information from the /me endpoint @raw_info ||= access_token.get('https://graph.microsoft.com/v1.0/me?$select=displayName,mail,mailboxSettings,userPrincipalName').parsed end def authorize_params super.tap do |params| params[:scope] = request.params['scope'] if request.params['scope'] params[:scope] ||= DEFAULT_SCOPE end end # Override callback URL # OmniAuth by default passes the entire URL of the callback, including # query parameters. Azure fails validation because that doesn't match the # registered callback. def callback_url options[:redirect_uri] || (full_host + script_name + callback_path) end end end end
花些时间查看此代码执行哪些功能。
- 它将 设置
client_options
以指定Microsoft 标识平台终结点。 - 它指定应在
scope
授权阶段发送参数。 - 它将
id
用户的属性映射为用户的唯一 ID。 - 它使用访问令牌从 Microsoft Graph检索用户配置文件以填写
raw_info
哈希。 - 它会替代回调 URL,以确保它与应用注册门户中注册的回调匹配。
- 它将 设置
在
omniauth_graph.rb
./config/initializers 文件夹中创建名为 的新文件,并添加以下代码。require 'microsoft_graph_auth' Rails.application.config.middleware.use OmniAuth::Builder do provider :microsoft_graph_auth, ENV['AZURE_APP_ID'], ENV['AZURE_APP_SECRET'], :scope => ENV['AZURE_SCOPES'] end
此代码将在应用启动时执行。 它使用提供程序加载 OmniAuth 中间件,该中间件使用
microsoft_graph_auth
oauth_environment_variables.rb 中设置的环境变量进行配置。
实施登录
现已配置 OmniAuth 中间件,可以继续向应用添加登录。
在 CLI 中运行以下命令,生成用于登录和注销的控制器。
rails generate controller Auth
打开 ./app/controllers/auth_controller.rb。 将回调方法添加到
AuthController
类。 OAuth 流完成后,OmniAuth 中间件将调用此方法。def callback # Access the authentication hash for omniauth data = request.env['omniauth.auth'] # Temporary for testing! render json: data.to_json end
现在,这一切就是呈现 OmniAuth 提供的哈希。 在继续之前,你将使用此验证登录是否正常工作。
将路由添加到 ./config/routes.rb。
# Add route for OmniAuth callback match '/auth/:provider/callback', :to => 'auth#callback', :via => [:get, :post]
启动服务器并浏览到
https://localhost:3000
。 单击登录按钮,你应被重定向到https://login.microsoftonline.com
。 使用 Microsoft 帐户登录并同意请求的权限。 浏览器重定向到应用,显示 OmniAuth 生成的哈希。{ "provider": "microsoft_graph_auth", "uid": "eb52b3b2-c4ac-4b4f-bacd-d5f7ece55df0", "info": { "name": null }, "credentials": { "token": "eyJ0eXAi...", "refresh_token": "OAQABAAA...", "expires_at": 1529517383, "expires": true }, "extra": { "raw_info": { "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users(displayName,mail,mailboxSettings,userPrincipalName)/$entity", "displayName": "Lynne Robbins", "mail": "LynneR@contoso.OnMicrosoft.com", "userPrincipalName": "LynneR@contoso.OnMicrosoft.com", "id": "d294e784-840e-4f9f-bb1e-95c0a75f2f18@2d18179c-4386-4cbd-8891-7fd867c4f62e", "mailboxSettings": { "archiveFolder": "AAMkAGI2...", "timeZone": "Pacific Standard Time", "delegateMeetingMessageDeliveryOptions": "sendToDelegateOnly", "dateFormat": "M/d/yyyy", "timeFormat": "h:mm tt", "automaticRepliesSetting": { "status": "disabled", "externalAudience": "all", "internalReplyMessage": "", "externalReplyMessage": "", "scheduledStartDateTime": { "dateTime": "2020-12-09T17:00:00.0000000", "timeZone": "UTC" }, "scheduledEndDateTime": { "dateTime": "2020-12-10T17:00:00.0000000", "timeZone": "UTC" } }, "language": { "locale": "en-US", "displayName": "English (United States)" }, "workingHours": { "daysOfWeek": [ "monday", "tuesday", "wednesday", "thursday", "friday" ], "startTime": "08:00:00.0000000", "endTime": "17:00:00.0000000", "timeZone": { "name": "Pacific Standard Time" } } } } } }
存储令牌
现在你可以获取令牌,可以执行在应用程序中存储令牌的方法了。 由于这是一个示例应用,为简单起见,你将在会话中存储它们。 现实应用程序会使用更可靠的安全存储解决方案,如数据库。
打开 ./app/controllers/application_controller.rb。 将下列方法添加到
ApplicationController
类。def save_in_session(auth_hash) # Save the token info session[:graph_token_hash] = auth_hash[:credentials] # Save the user's display name session[:user_name] = auth_hash.dig(:extra, :raw_info, :displayName) # Save the user's email address # Use the mail field first. If that's empty, fall back on # userPrincipalName session[:user_email] = auth_hash.dig(:extra, :raw_info, :mail) || auth_hash.dig(:extra, :raw_info, :userPrincipalName) # Save the user's time zone session[:user_timezone] = auth_hash.dig(:extra, :raw_info, :mailboxSettings, :timeZone) end
此方法将 OmniAuth 哈希作为参数,并提取相关信息位,然后将该信息存储在会话中。
将访问器函数添加到 类,以从会话检索回的用户名、
ApplicationController
电子邮件地址和访问令牌。def user_name session[:user_name] end def user_email session[:user_email] end def user_timezone session[:user_timezone] end def access_token session[:graph_token_hash][:token] end
将以下代码添加到
ApplicationController
将在处理任何操作之前运行的类。before_action :set_user def set_user @user_name = user_name @user_email = user_email end
此方法设置布局在 (l.erb application.htm中用于) 导航栏中显示用户信息的变量。 通过在此处添加它,不必在单个控制器操作中添加此代码。 但是,此操作也将针对 中的操作运行
AuthController
,这不是最佳操作。将以下代码添加到
AuthController
./app/controllers/auth_controller.rb 中的 类,以跳过 before 操作。skip_before_action :set_user
更新
callback
类中的AuthController
函数以在会话中存储令牌并重定向回主页。 将现有的callback
函数替换为以下内容。def callback # Access the authentication hash for omniauth data = request.env['omniauth.auth'] # Save the data in the session save_in_session data redirect_to root_url end
实现注销
测试此新功能之前,请添加一种注销方法。
将以下操作添加到
AuthController
类。def signout reset_session redirect_to root_url end
将此操作添加到 ./config/routes.rb。
get 'auth/signout'
重新启动服务器并完成登录过程。 你最终应返回主页,但 UI 应更改以指示你已登录。
单击右上角的用户头像以访问 "注销" 链接。 单击 "注销 "将重置会话,并返回到主页。
刷新令牌
如果你仔细查看 OmniAuth 生成的哈希,你会注意到哈希中存在两个标记: token
和 refresh_token
。 中的 token
值为访问令牌,在 API 调用 Authorization
标头中发送。 这是允许应用代表用户访问 Microsoft Graph令牌。
但是,此令牌期很短。 令牌在颁发后一小时过期。 这是值 refresh_token
变得有用的位置。 刷新令牌允许应用程序请求新的访问令牌,而无需用户再次登录。 更新令牌管理代码以实施令牌刷新。
打开 ./app/controllers/application_controller.rb, 在顶部
require
添加以下语句:require 'microsoft_graph_auth' require 'oauth2'
将下列方法添加到
ApplicationController
类。def refresh_tokens(token_hash) oauth_strategy = OmniAuth::Strategies::MicrosoftGraphAuth.new( nil, ENV['AZURE_APP_ID'], ENV['AZURE_APP_SECRET'] ) token = OAuth2::AccessToken.new( oauth_strategy.client, token_hash[:token], :refresh_token => token_hash[:refresh_token] ) # Refresh the tokens new_tokens = token.refresh!.to_hash.slice(:access_token, :refresh_token, :expires_at) # Rename token key new_tokens[:token] = new_tokens.delete :access_token # Store the new hash session[:graph_token_hash] = new_tokens end
此方法使用 oauth2 gem (依赖) 刷新令牌并
omniauth-oauth2
更新会话。将当前
access_token
方法替换为以下内容。def access_token token_hash = session[:graph_token_hash] # Get the expiry time - 5 minutes expiry = Time.at(token_hash[:expires_at] - 300) if Time.now > expiry # Token expired, refresh new_hash = refresh_tokens token_hash new_hash[:token] else token_hash[:token] end end
它将首先检查令牌是否接近过期,而不是仅从会话返回令牌。 如果是,它将在返回令牌之前刷新。
获取日历视图
在此练习中,你将将 Microsoft Graph合并到应用程序中。 对于此应用程序,你将使用httparty gem 调用 Microsoft Graph。
创建Graph帮助程序
创建用于管理所有 API 调用的帮助程序。 在 CLI 中运行以下命令以生成帮助程序。
rails generate helper Graph
打开 ./app/helpers/graph_helper.rb, 将内容替换为以下内容。
require 'httparty' # Graph API helper methods module GraphHelper GRAPH_HOST = 'https://graph.microsoft.com'.freeze def make_api_call(method, endpoint, token, headers = nil, params = nil, payload = nil) headers ||= {} headers[:Authorization] = "Bearer #{token}" headers[:Accept] = 'application/json' params ||= {} case method.upcase when 'GET' HTTParty.get "#{GRAPH_HOST}#{endpoint}", :headers => headers, :query => params when 'POST' headers['Content-Type'] = 'application/json' HTTParty.post "#{GRAPH_HOST}#{endpoint}", :headers => headers, :query => params, :body => payload ? payload.to_json : nil else raise "HTTP method #{method.upcase} not implemented" end end end
花些时间查看此代码执行哪些功能。 它通过 gem 向请求的终结点进行简单的 GET 或 POST httparty
请求。 它将在 标头中发送访问 Authorization
令牌,并包括传递的任何查询参数。
例如,若要使用 make_api_call
方法执行 GET 操作, https://graph.microsoft.com/v1.0/me?$select=displayName
可以如下所示调用它:
make_api_call 'GET', '/v1.0/me', access_token, {}, { '$select': 'displayName' }
当你在应用中实现更多 Microsoft Graph功能时,你稍后将基于这一点进行构建。
从 Outlook 获取日历事件
在 CLI 中,运行以下命令以添加新控制器。
rails generate controller Calendar index new
将新路由添加到 ./config/routes.rb。
get 'calendar', :to => 'calendar#index'
向日历帮助程序Graph一个获取日历视图的方法。 打开 ./app/helpers/graph_helper.rb, 然后向模块中添加以下
GraphHelper
方法。def get_calendar_view(token, start_datetime, end_datetime, timezone) get_events_url = '/v1.0/me/calendarview' headers = { 'Prefer' => "outlook.timezone=\"#{timezone}\"" } query = { 'startDateTime' => start_datetime.iso8601, 'endDateTime' => end_datetime.iso8601, '$select' => 'subject,organizer,start,end', '$orderby' => 'start/dateTime', '$top' => 50 } response = make_api_call 'GET', get_events_url, token, headers, query raise response.parsed_response.to_s || "Request returned #{response.code}" unless response.code == 200 response.parsed_response['value'] end
考虑此代码将执行什么工作。
- 将调用的 URL 为
/v1.0/me/calendarview
。Prefer: outlook.timezone
标头使结果中的开始时间和结束时间根据用户的时区进行调整。- 和
startDateTime
endDateTime
参数设置视图的起始和结束。 - 参数
$select
将每个事件返回的字段限制为视图将实际使用的字段。 - 参数
$orderby
按开始时间对结果进行排序。 - 参数
$top
将结果限制为 50 个事件。
- 为了成功响应,它将返回键中包含的项目
value
数组。
- 将调用的 URL 为
向帮助程序添加新Graph,以根据时区名称Windows IANA时区标识符。 这是必需的,因为 Microsoft Graph可以将时区作为Windows时区名称返回,并且 Ruby DateTime 类需要 IANA 时区标识符。
TIME_ZONE_MAP = { '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' }.freeze def get_iana_from_windows(windows_tz_name) iana = TIME_ZONE_MAP[windows_tz_name] # If no mapping found, assume the supplied # value was already an IANA identifier iana || windows_tz_name end
打开 ./app/controllers/calendar_controller.rb, 并将其全部内容替换为以下内容。
# Calendar controller class CalendarController < ApplicationController include GraphHelper def index # Get the IANA identifier of the user's time zone time_zone = get_iana_from_windows(user_timezone) # Calculate the start and end of week in the user's time zone start_datetime = Date.today.beginning_of_week(:sunday).in_time_zone(time_zone).to_time end_datetime = start_datetime.advance(:days => 7) @events = get_calendar_view access_token, start_datetime, end_datetime, user_timezone || [] render json: @events rescue RuntimeError => e @errors = [ { :message => 'Microsoft Graph returned an error getting events.', :debug => e } ] end end
重新启动服务器。 登录并单击导航 栏中 的"日历"链接。 如果一切正常,应在用户日历上看到事件被 JSON 卸载。
显示结果
现在,您可以添加 HTML 以更用户友好的方式显示结果。
打开 ./app/views/calendar/index.html.erb, 并将其内容替换为以下内容。
<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> <% @events.each do |event| %> <tr> <td><%= event['organizer']['emailAddress']['name'] %></td> <td><%= event['subject'] %></td> <td><%= event['start']['dateTime'].to_time(:utc).strftime('%-m/%-d/%Y %l:%M %p') %></td> <td><%= event['end']['dateTime'].to_time(:utc).strftime('%-m/%-d/%Y %l:%M %p') %></td> </tr> <% end %> </tbody> </table>
该操作将循环遍历事件集合,并针对每个事件添加一个表格行。
从
render json: @events
index
./app/controllers/calendar_controller.rb 中的操作中删除行。刷新页面,应用现在应呈现事件表。
创建新事件
在此部分中,您将添加在用户日历上创建事件的能力。
打开 ./app/helpers/graph_helper.rb, 将以下方法添加到 Graph 类。
def create_event(token, timezone, subject, start_datetime, end_datetime, attendees, body) create_event_url = '/v1.0/me/events' # Create an event object # https://docs.microsoft.com/graph/api/resources/event?view=graph-rest-1.0 new_event = { 'subject' => subject, 'start' => { 'dateTime' => start_datetime, 'timeZone' => timezone }, 'end' => { 'dateTime' => end_datetime, 'timeZone' => timezone } } unless attendees.empty? attendee_array = [] # Create an attendee object # https://docs.microsoft.com/graph/api/resources/attendee?view=graph-rest-1.0 attendees.each { |email| attendee_array.push({ 'type' => 'required', 'emailAddress' => { 'address' => email } }) } new_event['attendees'] = attendee_array end unless body.empty? # Create an itemBody object # https://docs.microsoft.com/graph/api/resources/itembody?view=graph-rest-1.0 new_event['body'] = { 'contentType' => 'text', 'content' => body } end response = make_api_call 'POST', create_event_url, token, nil, nil, new_event raise response.parsed_response.to_s || "Request returned #{response.code}" unless response.code == 201 end
打开 "./app/controllers/calendar_controller", 将以下路由添加到 CalendarController 类。
def create # Semicolon-delimited list, split to an array attendees = params[:ev_attendees].split(';') # Create the event create_event access_token, user_timezone, params[:ev_subject], params[:ev_start], params[:ev_end], attendees, params[:ev_body] # Redirect back to the calendar list redirect_to({ :action => 'index' }) rescue RuntimeError => e @errors = [ { :message => 'Microsoft Graph returned an error creating the event.', :debug => e } ] end
打开 ./config/routes.rb 并添加新路由。
post 'calendar/new', :to => 'calendar#create'
打开 ./app/views/calendar/new.html.erb, 并将其内容替换为以下内容。
<h1>New event</h1> <%= form_with url: "/calendar/new" do |form| %> <div class="form-group"> <%= form.label :ev_subject, "Subject" %> <%= form.text_field :ev_subject, class: "form-control", required: true %> </div> <div class="form-group"> <%= form.label :ev_attendees, "Attendees" %> <%= form.text_field :ev_attendees, class: "form-control", placeholder: "Separate multiple email addresses with a semicolon (';')" %> </div> <div class="form-row"> <div class="col"> <div class="form-group"> <%= form.label :ev_start, "Start" %> <%= form.datetime_local_field :ev_start, class: "form-control", required: true %> </div> </div> <div class="col"> <div class="form-group"> <%= form.label :ev_end, "End" %> <%= form.datetime_local_field :ev_end, class: "form-control", required: true %> </div> </div> </div> <div class="form-group mb-3"> <%= form.label :ev_body, "Body" %> <%= form.text_area :ev_body, class: "form-control", rows: "3" %> </div> <%= form.submit "Create", class: "btn btn-primary mr-2" %> <a class="btn btn-secondary" href="/calendar">Cancel</a> <% end %>
保存更改并刷新应用。 在" 日历" 页上,选择" 新建事件" 按钮。 填写表单并选择" 创建" 以创建新事件。
恭喜!
你已完成 Ruby Microsoft Graph教程。 现在,你已经拥有一个调用 Microsoft Graph,你可以试验和添加新功能。 请访问Microsoft Graph概述,查看可以使用 Microsoft Graph 访问的所有数据。
反馈
Please provide any feedback on this tutorial in the GitHub repository.
你有关于此部分的问题? 如果有,请向我们提供反馈,以便我们对此部分作出改进。