Skip to content

Instantly share code, notes, and snippets.

@jmb
Last active December 8, 2022 08:41
Show Gist options
  • Star 15 You must be signed in to star a gist
  • Fork 4 You must be signed in to fork a gist
  • Save jmb/33ae3a33d6e8dbffd102 to your computer and use it in GitHub Desktop.
Save jmb/33ae3a33d6e8dbffd102 to your computer and use it in GitHub Desktop.
Google Calendar API v3 widget for Dashing

Description

Dashing widget to display the next and some subsequent Google Calendar events using the Google Calendar API v3.

I use this widget to display my shift calendar - see the screenshot below

Set up

This widget works with API v3 and requires a service account to be set up via the Google Developer's Console. Once a project is set up, enable the Calendar API. On the Credentials page create a new OpenID and download the p12 key file - set up the path to this file in the job file and grant the email address access to the relevant calendar.

The job file defines how many events to get from the calendar and when to start the search. My version gets the next 6 events.

Add to Gemfile:

gem 'google-api-client', '>= 0.8'

Run

bundle install

Download the Moment javascript library and add to your javascript assets. Add #= require moment.js to the application.coffee script. You can also use moment.min.js if you've downloaded that.

class Dashing.GoogleCalendar extends Dashing.Widget
onData: (data) =>
event = rest = null
getEvents = (first, others...) ->
event = first
rest = others
getEvents data.events.items...
start = moment(event.start.dateTime)
end = moment(event.end.dateTime)
@set('event',event)
@set('event_date', start.format('dddd Do MMMM'))
@set('event_times', start.format('HH:mm') + " - " + end.format('HH:mm'))
next_events = []
for next_event in rest
start = moment(next_event.start.dateTime)
start_date = start.format('ddd Do MMM')
start_time = start.format('HH:mm')
next_events.push { summary: next_event.summary, start_date: start_date, start_time: start_time }
@set('next_events', next_events)
<h1 class="subtitle" >Next event:</h1>
<h3 class="times" data-bind="event_date"></h3>
<h2 class="title" data-bind="event.summary"></h2>
<h3 class="times" data-bind="event_times"></h3>
<h4 data-bind="next_count"></h4>
<table class="next">
<tr data-foreach-e='next_events'>
<td data-bind="e.start_date"></td>
<td data-bind="e.start_time"></td>
<td data-bind="e.summary"></td>
</tr>
</table>
<div class="updated-at" data-bind="updatedAtMessage"></div>
# encoding: UTF-8
require 'google/api_client'
require 'date'
require 'time'
require 'digest/md5'
require 'active_support'
require 'active_support/all'
require 'json'
# Update these to match your own apps credentials
service_account_email = '...@developer.gserviceaccount.com' # Email of service account
key_file = '/Path/to/keyfile.p12' # File containing your private key
key_secret = 'notasecret' # Password to unlock private key
calendarID = '...@group.calendar.google.com' # Calendar ID.
# Get the Google API client
client = Google::APIClient.new(:application_name => 'Dashing Calendar Widget',
:application_version => '0.0.1')
# Load your credentials for the service account
if not key_file.nil? and File.exists? key_file
key = Google::APIClient::KeyUtils.load_from_pkcs12(key_file, key_secret)
else
key = OpenSSL::PKey::RSA.new ENV['GOOGLE_SERVICE_PK'], key_secret
end
client.authorization = Signet::OAuth2::Client.new(
:token_credential_uri => 'https://accounts.google.com/o/oauth2/token',
:audience => 'https://accounts.google.com/o/oauth2/token',
:scope => 'https://www.googleapis.com/auth/calendar.readonly',
:issuer => service_account_email,
:signing_key => key)
# Start the scheduler
SCHEDULER.every '15m', :first_in => 4 do |job|
# Request a token for our service account
client.authorization.fetch_access_token!
# Get the calendar API
service = client.discovered_api('calendar','v3')
# Start and end dates
now = DateTime.now
result = client.execute(:api_method => service.events.list,
:parameters => {'calendarId' => calendarID,
'timeMin' => now.rfc3339,
'orderBy' => 'startTime',
'singleEvents' => 'true',
'maxResults' => 6}) # How many calendar items to get
send_event('google_calendar', { events: result.data })
end
// ----------------------------------------------------------------------------
// Sass declarations
// ----------------------------------------------------------------------------
$background-color: #47bbb3; //#ec663c;
$title-color: rgba(255, 255, 255, 1);
$subtitle-color: rgba(255, 255, 255, 0.7);
$moreinfo-color: rgba(255, 255, 255, 0.7);
// ----------------------------------------------------------------------------
// Widget-calendar styles
// ----------------------------------------------------------------------------
.widget-google-calendar {
background-color: $background-color;
.subtitle {
color: $subtitle-color;
font-size: 0.75em;
margin: 15px 0;
}
.title {
color: $title-color;
font-size: 1.4em;
}
.times {
font-size: 0.9em;
}
.next {
font-size: 0.75em;
margin-top: 30px;
.tr {
border-bottom: 1px solid $subtitle-color;
.td {
text-align: left;
}
}
}
.more-info {
color: $moreinfo-color;
}
.updated-at {
color: $subtitle-color;
bottom: 5px;
right: 5px;
left: auto;
font-size: 0.5em;
}
&.large h3 {
font-size: 65px;
}
}
@michaelgra
Copy link

Can anyone possibly help a newbie with the following error "google_calendar.rb:25:in `initialize': can't convert nil into String (TypeError)"

I'm pretty sure I've got the correct path to the key in with the password, not sure where I'm going wrong

@kush4all
Copy link

kush4all commented Apr 1, 2015

This has a problem with all day events, where you don't specify start or end time. Works really good when you have events with time. I am getting today's date and time for all All day events. Please help.

@qlaffont
Copy link

qlaffont commented May 9, 2015

It's impossible for me. I have the same problem. No data is displayed. I just get the blank widget with "Next Event:" on it. I do all your process and i fail... What is the problem ?

@kush4all
Copy link

kush4all commented May 9, 2015

I think you are forgetting to give your service account calendar access. I had same issue. You have to share calendar with your service account. Let me know. JMB, please help me in fixing all day events. (Which don't have start and end time specified.) Thank you for making this widget with Google authentication.

@jsyeo
Copy link

jsyeo commented May 27, 2015

I forked your gist to fetch from the calendar's private address instead. I hope you are cool with that. 😄 https://gist.github.com/jsyeo/39d3fde3afbffdd31093

@coderaver
Copy link

Hi guys! So I've got this widget working and showing upcoming events for my calendar. Easy! However, the respective Morning/Afternoon/Evening part of events aren't showing up. Looking through the html it looks like this is the event.summary. I ended up using moment.min.js because I already had it installed for another widget. Could this have something to do with it? Code newbie here so have patience. Thank you

@PeterBi87
Copy link

I got the same problem as @michaelgra

/google_calendar.rb:25:in `initialize': can't convert nil into String (TypeError)

The path to the p12 file has to be the comple path including the dashing path or only the path out of the dashboard path? (/jobs/p12.p12 or /opt/mydashboardproject/jobs/p12.12 ? )

tried both, but it didnt fix the error.

maybe i did something wrong in here?:

Update these to match your own apps credentials

service_account_email = 'BLACKENED@developer.gserviceaccount.com' # Email of service account
key_file = '/opt/mydashboardproject/dashing-google-p12.p12' # File containing your private key
key_secret = 'notasecret' # Password to unlock private key
calendarID = 'BLACKENED%40group.calendar.google.com' # Calendar ID.

BLACKENED: i replaced the numbers and letters with BLACKENED ...

Any idea?

@mtwalsh
Copy link

mtwalsh commented Oct 20, 2015

@coderaver How did you get this working?

I, as well as others, can't seem to get passed this error:

google_calendar.rb:25:in `initialize': no implicit conversion of nil into String (TypeError)

Were you able to resolve this?

Any advice gratefully received!

@mtwalsh
Copy link

mtwalsh commented Oct 20, 2015

Ah, it was the path to the key file that was wrong, it should be relative to dashing's root folder (i.e. the parent folder to /jobs).

@philippeckel
Copy link

@kush4all

I simply added a start and end date time to every event in the ruby script:

  result = client.execute!(
                          :api_method => calendar_api.events.list,
                          :parameters => {
                          :calendarId => ENV['CALENDAR'],
                                          :timeMin => Time.now.iso8601,
                                          :orderBy => 'startTime',
                                          :singleEvents => 'true',
                                          :maxResults => 5,
                                          :showDeleted => 'false'
                                          })

  result.data.items.each do |event|

  if event.start.date_time == nil
  event.start.date_time = event.start.date + " 0:00:00 +0100"
  end

  if event.end.date_time == nil
  event.end.date_time = event.end.date + " 0:00:00 +0100"
  end

  end

All day events then are displayed all day long.

@philippeckel
Copy link

@kush4all forgot to include the last two lines

  send_event('google_calendar', { events: result.data })

end

@mtwalsh
Copy link

mtwalsh commented Jan 7, 2016

Thanks for that fix @peckeltw, worked for me.

@philippeckel
Copy link

You're welcome, @mtwalsh!

@sebstr1
Copy link

sebstr1 commented Feb 11, 2016

Great job with this widget!

It is working as it should except that I have special letters in my language but it is just these 3 chars: å ä ö. These 3 are very common Swedish letters. So whenever there is an event containing one of those letters the widget breaks and displays nothing with the following error from logs:

scheduler caught exception:
"\xC3" from ASCII-8BIT to UTF-8
/home/pi/.rbenv/versions/1.9.3-p448/lib/ruby/gems/1.9.1/gems/dashing-1.3.4/lib/dashing/app.rb:129:in `to_json'
/home/pi/.rbenv/versions/1.9.3-p448/lib/ruby/gems/1.9.1/gems/dashing-1.3.4/lib/dashing/app.rb:129:in `send_event'
/opt/dashboard/jobs/google_calendar.rb:28:in `block in <top (required)>'
/home/pi/.rbenv/versions/1.9.3-p448/lib/ruby/gems/1.9.1/gems/rufus-scheduler-2.0.24/lib/rufus/sc/jobs.rb:232:in `call'
/home/pi/.rbenv/versions/1.9.3-p448/lib/ruby/gems/1.9.1/gems/rufus-scheduler-2.0.24/lib/rufus/sc/jobs.rb:232:in `trigger_block'
/home/pi/.rbenv/versions/1.9.3-p448/lib/ruby/gems/1.9.1/gems/rufus-scheduler-2.0.24/lib/rufus/sc/jobs.rb:206:in `block in trigger'
/home/pi/.rbenv/versions/1.9.3-p448/lib/ruby/gems/1.9.1/gems/rufus-scheduler-2.0.24/lib/rufus/sc/scheduler.rb:431:in `call'
/home/pi/.rbenv/versions/1.9.3-p448/lib/ruby/gems/1.9.1/gems/rufus-scheduler-2.0.24/lib/rufus/sc/scheduler.rb:431:in `block in trigger_job'

If I remove the event with one of those letters, error goes away. I've been trying to fix this for the past 2 hours with zero success. Help anyone?

Best case: The widget displays the text with the special chars (å ä ö) Worst case: The ö is converted to o and å ä to a

@jstapley
Copy link

jstapley commented Mar 6, 2016

Has anyone seen this error before? I have been using this widget for 12 months and then I decided to move my dashboard to the cloud and I'm getting the following error when I try to start the dashboard with this widget.

`root@ubuntu-512mb-tor1-01:~/dashboard# dashing start

/var/lib/gems/1.9.1/gems/backports-3.6.8/lib/backports/std_lib.rb:9:in `require': /var/lib/gems/1.9.1/gems/jwt-1.5.3/lib/jwt/verify.rb:7: unknown type of %string (SyntaxError)
%i[verify_aud verify_expiration ...

/var/lib/gems/1.9.1/gems/jwt-1.5.3/lib/jwt/verify.rb:7: syntax error, unexpected $end
%i[verify_aud verify_expiration ...

from /var/lib/gems/1.9.1/gems/backports-3.6.8/lib/backports/std_lib.rb:9:in `require_with_backports'
from /var/lib/gems/1.9.1/gems/jwt-1.5.3/lib/jwt/decode.rb:2:in `<top (required)>'
from /var/lib/gems/1.9.1/gems/backports-3.6.8/lib/backports/std_lib.rb:9:in `require'
from /var/lib/gems/1.9.1/gems/backports-3.6.8/lib/backports/std_lib.rb:9:in `require_with_backports'
from /var/lib/gems/1.9.1/gems/jwt-1.5.3/lib/jwt.rb:3:in `<top (required)>'
from /var/lib/gems/1.9.1/gems/backports-3.6.8/lib/backports/std_lib.rb:9:in `require'
from /var/lib/gems/1.9.1/gems/backports-3.6.8/lib/backports/std_lib.rb:9:in `require_with_backports'
from /var/lib/gems/1.9.1/gems/google-api-client-0.8.2/lib/google/api_client/auth/jwt_asserter.rb:15:in `<top (required)>'
from /var/lib/gems/1.9.1/gems/backports-3.6.8/lib/backports/std_lib.rb:9:in `require'
from /var/lib/gems/1.9.1/gems/backports-3.6.8/lib/backports/std_lib.rb:9:in `require_with_backports'
from /var/lib/gems/1.9.1/gems/google-api-client-0.8.2/lib/google/api_client/service_account.rb:16:in `<top (required)>'
from /var/lib/gems/1.9.1/gems/backports-3.6.8/lib/backports/std_lib.rb:9:in `require'
from /var/lib/gems/1.9.1/gems/backports-3.6.8/lib/backports/std_lib.rb:9:in `require_with_backports'
from /var/lib/gems/1.9.1/gems/google-api-client-0.8.2/lib/google/api_client.rb:31:in `<top (required)>'
from /var/lib/gems/1.9.1/gems/backports-3.6.8/lib/backports/std_lib.rb:9:in `require'
from /var/lib/gems/1.9.1/gems/backports-3.6.8/lib/backports/std_lib.rb:9:in `require_with_backports'
from /root/dashboard/jobs/google_calendar_family.rb:3:in `<top (required)>'
from /var/lib/gems/1.9.1/gems/backports-3.6.8/lib/backports/std_lib.rb:9:in `require'
from /var/lib/gems/1.9.1/gems/backports-3.6.8/lib/backports/std_lib.rb:9:in `require_with_backports'
from /var/lib/gems/1.9.1/gems/dashing-1.3.4/lib/dashing/app.rb:157:in `block in require_glob'
from /var/lib/gems/1.9.1/gems/dashing-1.3.4/lib/dashing/app.rb:156:in `each'
from /var/lib/gems/1.9.1/gems/dashing-1.3.4/lib/dashing/app.rb:156:in `require_glob'
from /var/lib/gems/1.9.1/gems/dashing-1.3.4/lib/dashing/app.rb:167:in `<top (required)>'
from /var/lib/gems/1.9.1/gems/dashing-1.3.4/lib/dashing.rb:3:in `require'
from /var/lib/gems/1.9.1/gems/dashing-1.3.4/lib/dashing.rb:3:in `<top (required)>'
from config.ru:1:in `require'
from config.ru:1:in `block in <main>'
from /var/lib/gems/1.9.1/gems/rack-1.5.5/lib/rack/builder.rb:55:in `instance_eval'
from /var/lib/gems/1.9.1/gems/rack-1.5.5/lib/rack/builder.rb:55:in `initialize'
from config.ru:1:in `new'
from config.ru:1:in `<main>'
from /var/lib/gems/1.9.1/gems/thin-1.6.4/lib/rack/adapter/loader.rb:33:in `eval'
from /var/lib/gems/1.9.1/gems/thin-1.6.4/lib/rack/adapter/loader.rb:33:in `load'
from /var/lib/gems/1.9.1/gems/thin-1.6.4/lib/thin/controllers/controller.rb:182:in `load_rackup_config'
from /var/lib/gems/1.9.1/gems/thin-1.6.4/lib/thin/controllers/controller.rb:72:in `start'
from /var/lib/gems/1.9.1/gems/thin-1.6.4/lib/thin/runner.rb:200:in `run_command'
from /var/lib/gems/1.9.1/gems/thin-1.6.4/lib/thin/runner.rb:156:in `run!'
from /var/lib/gems/1.9.1/gems/thin-1.6.4/bin/thin:6:in `<top (required)>'
from /usr/local/bin/thin:23:in `load'
from /usr/local/bin/thin:23:in `<main>'

`

@hkmaly
Copy link

hkmaly commented Mar 17, 2016

jstapley: You need to use version 1.5.2. Version 1.5.3 is broken, see jwt/ruby-jwt#132

@n3rdz
Copy link

n3rdz commented Apr 9, 2016

same here, how can i fix this? how can i load and use v 1.5.2 ?

@sash85
Copy link

sash85 commented Apr 20, 2016

@n3rdz an new version is out and should work. You have to upgrade your Gemfile to ruby 1.5.4.

@sash85
Copy link

sash85 commented Apr 20, 2016

Is it possible to limit the length of the calendar listings? My layout get broken if a entry is to long.

@dazayas
Copy link

dazayas commented Apr 21, 2016

Suppose I wanted, for all-day events, to have the widget display "All Day" instead of start and end times of 12:00 AM. What would be the best way to implement that?

@vageesh79
Copy link

I am not getting any error while starting dashing but in dashboard instead of calendar
capture
i am getting some template code, What i am doing wrong any ideas.

@pryonic
Copy link

pryonic commented Jun 5, 2016

This looks exactly what I need - unfortunately I can't get it working. I've followed the steps I believe correctly, and the widget displays but does not show any events.

There's also no errors in the logs - which makes me think I've made mistake on the Google API side of things. Has anything changed here recently? I've set up the job as:

# Update these to match your own apps credentials
service_account_email = 'blahblah-service-account@home-control-panel.iam.gserviceaccount.com' # Email of service account
key_file = '/home/pi/hcp.p12' # File containing your private key
key_secret = 'notasecret' # Password to unlock private key
calendarID = 'blah.blah@google.com' # Calendar ID.

I've enabled the calendar API I believe - but do I need to somehow grant the app access to the calendar blah.blah@google.com? One thing I am seeing is that when I refresh the dashboard the "error count" for "Client Errors" on the Google Calendar API page is going up.

Any idea where I can go from here?

@pryonic
Copy link

pryonic commented Jun 5, 2016

Bit more info - I took a copy of the google_calendar.rb file and instead of trying to pass the data retrieved from the Google API i just printed it. I'm not a Ruby programmer at all, but hopefully this is right:

print events: result.data

This is giving me:

{:events=>#<Google::APIClient::Schema::Calendar::V3::Events:0x8ccc4c DATA:{"error"=>{"errors"=>[{"domain"=>"global", "reason"=>"notFound", "message"=>"Not Found"}], "code"=>404, "message"=>"Not Found"}}>}
Something's 404ing, which matches the errors I'm seeing on the Google API screen.

The P12 file and key file are related to the service account yes? Because it's not possible to download P12 files for OAuth 2.0 accounts, only the service accounts

@boolsee
Copy link

boolsee commented Aug 19, 2016

@pryonic I met the same problem, but I solved it finally. I tested the API parameters one-by-one by using "Google API Explorer for Calendar" .
I hope you solved already.

@boolsee
Copy link

boolsee commented Aug 19, 2016

I modified to view single event first and all day event later which is based on @peckeltw code. Because I noticed that the start and end date of all day event is not the same, I changed the end date of all day event to the previous date like this.

event.end.date = (Date.parse(event.end.date)-1).to_s

Now, the end date of all day event will be like this:

ex1) all day event for 1 day (shown on 1 day)
   As-is:  2016-08-19 ~ 2016-08-20  
   To-be:  2016-08-19 ~ 2016-08-19

ex2) all day event for 2 day and more (shown on 2 days)
  As-is:  2016-08-19 ~ 2016-08-21  
  To-be:  2016-08-19 ~ 2016-08-20

Full code is below:

# Temp Array for sort
tmp1 = Array.new # for all day events
tmp2 = Array.new # for single event

 # All day event ?
result.data.items.each do |event|

    all_day_event = 0

    # Append date_time for the all-day event
    if event.start.date_time == nil
        event.start.date_time = event.start.date + " 9:00:00 +0900"
        all_day_event = 1
    end

    if event.end.date_time == nil

        event.end.date = (Date.parse(event.end.date)-1).to_s
        event.end.date_time = event.end.date + " 18:00:00 +0900"
        all_day_event = 1
    end

    # Save the all day events to temp array
    if all_day_event == 1
        tmp1 << event
    else
        tmp2 << event
    end
end

@lanceDamage
Copy link

Newbie here...I'm having trouble understanding the following instructions. Can someone help explain these to me?

"Download the Moment javascript library and add to your javascript assets. Add #= require moment.js to the application.coffee script."

I understand how to download Moment, but I'm not sure where I am suppose to "add to your javascript assets." I am also not sure what is being referred to with "application.coffee script." Is that referring to adding a line of code in google_calendar.coffee? If not, can someone provide the path to this script? Thanks!

@wmreynolds
Copy link

getting a "LoadError: cannot load such file -- google/api_client"

@emilyboda
Copy link

emilyboda commented Mar 19, 2018

@lanceDamage you need to download this thing called moments and add it to /yourproject/assets/javascripts.
cd /yourproject/assets/javascripts
sudo wget https://momentjs.com/downloads/moment.js

Then in the same directory
sudo nano application.coffee
and then add "#= require moment.js" to the top under the other similar line.

@rnhall82
Copy link

@wmreynolds I got that same error. After looking around, I found it was the version of the google api client. From what I read, apparently they made some major changes in it. I was able to fix it by changing that section in the gemfile to read as so:

gem 'google-api-client', '< 0.9'

After I did that, i stopped getting that error message and everything started working for me.

@kny2tl
Copy link

kny2tl commented Jan 21, 2021

I'm struggling to get this running. Let me summary what's current setup (after changing ie Gemfile to gem 'google-api-client', '< 0.9')
Gemfile:

source 'https://rubygems.org'
gem 'smashing'
#gem 'icalendar'
#gem 'google-api-client', '~> 0.53.0'
#gem 'google-api-client', '>= 0.8'
gem 'google-api-client', '< 0.9'

application.coffee:

dashing.js is located in the dashing framework
It includes jquery & batman for you.
#= require dashing.js
#= require_directory .
#= require_tree ../../widgets
#= require moment.js
and so on

ls assets/javascripts/
application.coffee d3-3.2.8.js dashing.gridster.coffee gridster jquery.knob.js moment.js moment-with-locales.js rickshaw-1.4.3.min.js

google_calendar.rb:

Update these to match your own apps credentials
service_account_email = '@.iam.gserviceaccount.com' # Email of service account
key_file = '/home/pi/my-project/keyfile.p12' # File containing your private key
key_secret = 'notasecret' # Password to unlock private key
calendarID = '@google.com' # Calendar ID.

AFAIK, set up is fine.
API is allowed. Key was generated. Calendar is shared with that service account.

What I am getting is 'Next event:' but that's it. Enabling debugging I was rewarded with following

{ 753569630 rufus-scheduler intercepted an error:
  753569630   job:
  753569630     Rufus::Scheduler::EveryJob "15m" {:first_in=>4}
  753569630   error:
  753569630     753569630
  753569630     ArgumentError
  753569630     header field value cannot include CR/LF

with help of google I found that downgrade of ruby (now ruby 2.5.5p157 (2019-03-15 revision 67260) [i386-linux-gnu]) but this leads me rvm and other funnies - which would be task for upcoming days. Before going this path, I'm wondering if you guys have any hints.
thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment