sprout-google-spreadsheet-ruby 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +84 -0
- data/lib/google_spreadsheet.rb +809 -0
- metadata +77 -0
data/README.rdoc
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
This is a Ruby 1.8/1.9 library to read/write Google Spreadsheet.
|
2
|
+
|
3
|
+
|
4
|
+
= How to install
|
5
|
+
|
6
|
+
$ gem sources -a http://gems.github.com
|
7
|
+
$ sudo gem install gimite-google-spreadsheet-ruby
|
8
|
+
|
9
|
+
|
10
|
+
= How to use
|
11
|
+
|
12
|
+
Example:
|
13
|
+
|
14
|
+
require "rubygems"
|
15
|
+
require "google_spreadsheet"
|
16
|
+
|
17
|
+
# METHOD #1: With usual google username and password
|
18
|
+
# Logs in.
|
19
|
+
session = GoogleSpreadsheet.login("username@gmail.com", "mypassword")
|
20
|
+
|
21
|
+
# METHOD #2: Getting rid of usual google username and password,using google OAuth protocol
|
22
|
+
# 1) First generate OAuth consumer object with key and secret for your site by registering site with google
|
23
|
+
@consumer = OAuth::Consumer.new( "key","secret", {:site=>"https://agree2"})
|
24
|
+
|
25
|
+
# 2) Request token with OAuth
|
26
|
+
@request_token=@consumer.get_request_token
|
27
|
+
session[:request_token] = @request_token
|
28
|
+
redirect_to @request_token.authorize_url
|
29
|
+
|
30
|
+
# 3) Create an oauth access token
|
31
|
+
@oauth_access_token = @request_token.get_access_token
|
32
|
+
@access_token = OAuth::AccessToken.new(@consumer, @oauth_access_token.token, @oauth_access_token.secret)
|
33
|
+
|
34
|
+
|
35
|
+
# See these documents for details:
|
36
|
+
# http://oauth.rubyforge.org/
|
37
|
+
# http://code.google.com/apis/accounts/docs/OAuth.html
|
38
|
+
|
39
|
+
# Creates GoogleSpreadsheet object with access_token
|
40
|
+
session = GoogleSpreadsheet.authorized(@access_token)
|
41
|
+
|
42
|
+
# First worksheet of http://spreadsheets.google.com/ccc?key=pz7XtlQC-PYx-jrVMJErTcg&hl=en
|
43
|
+
ws = session.spreadsheet_by_key("pz7XtlQC-PYx-jrVMJErTcg").worksheets[0]
|
44
|
+
|
45
|
+
# Gets content of A2 cell.
|
46
|
+
p ws[2, 1] #==> "hoge"
|
47
|
+
|
48
|
+
# Changes content of cells. Changes are not sent to the server until you call ws.save().
|
49
|
+
ws[2, 1] = "foo"
|
50
|
+
ws[2, 2] = "bar"
|
51
|
+
ws.save()
|
52
|
+
|
53
|
+
# Dumps all cells.
|
54
|
+
for row in 1..ws.num_rows
|
55
|
+
for col in 1..ws.num_cols
|
56
|
+
p ws[row, col]
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Yet another way to do so.
|
61
|
+
p ws.rows #==> [["fuga", ""], ["foo", "bar]]
|
62
|
+
|
63
|
+
# Reloads the worksheet to get changes by other clients.
|
64
|
+
ws.reload()
|
65
|
+
|
66
|
+
|
67
|
+
|
68
|
+
API document: http://gimite.net/gimite/rubymess/google-spreadsheet-ruby/
|
69
|
+
|
70
|
+
|
71
|
+
= Source code
|
72
|
+
|
73
|
+
http://github.com/gimite/google-spreadsheet-ruby/tree/master
|
74
|
+
|
75
|
+
The license of this source is "New BSD Licence"
|
76
|
+
|
77
|
+
|
78
|
+
= Supported environments
|
79
|
+
|
80
|
+
Ruby 1.8.x and Ruby 1.9.x. Checked with Ruby 1.8.7 and Ruby 1.9.1.
|
81
|
+
|
82
|
+
|
83
|
+
= Author
|
84
|
+
Sprout Technology Services - http://sprout-technology.com
|
@@ -0,0 +1,809 @@
|
|
1
|
+
# Author: Hiroshi Ichikawa <http://gimite.net/>
|
2
|
+
# The license of this source is "New BSD Licence"
|
3
|
+
|
4
|
+
require "enumerator"
|
5
|
+
require "set"
|
6
|
+
require "net/https"
|
7
|
+
require "open-uri"
|
8
|
+
require "cgi"
|
9
|
+
require "uri"
|
10
|
+
require "rubygems"
|
11
|
+
require "hpricot"
|
12
|
+
require "oauth"
|
13
|
+
Net::HTTP.version_1_2
|
14
|
+
|
15
|
+
module GoogleSpreadsheet
|
16
|
+
|
17
|
+
# Authenticates with given +mail+ and +password+, and returns GoogleSpreadsheet::Session
|
18
|
+
# if succeeds. Raises GoogleSpreadsheet::AuthenticationError if fails.
|
19
|
+
# Google Apps account is supported.
|
20
|
+
def self.login(mail, password)
|
21
|
+
return Session.login(mail, password)
|
22
|
+
end
|
23
|
+
|
24
|
+
# If already authorized by Google then just pass in your oauth_token to begin
|
25
|
+
# For generating oauth_token,you can proceed as follow:
|
26
|
+
# 1) First generate OAuth consumer object with key and secret for your site by registering site with google
|
27
|
+
# @consumer = OAuth::Consumer.new( "key","secret", {:site=>"https://agree2"})
|
28
|
+
|
29
|
+
# 2) Request token with OAuth
|
30
|
+
# @request_token=@consumer.get_request_token
|
31
|
+
# session[:request_token] = @request_token
|
32
|
+
# redirect_to @request_token.authorize_url
|
33
|
+
|
34
|
+
# 3) Create an oauth access token
|
35
|
+
# @oauth_access_token = @request_token.get_access_token
|
36
|
+
# @access_token = OAuth::AccessToken.new(@consumer, @oauth_access_token.token, @oauth_access_token.secret)
|
37
|
+
|
38
|
+
# See these documents for details:
|
39
|
+
# http://oauth.rubyforge.org/
|
40
|
+
# http://code.google.com/apis/accounts/docs/OAuth.html
|
41
|
+
|
42
|
+
def self.authorized(oauth_token)
|
43
|
+
return Session.authorized(oauth_token)
|
44
|
+
end
|
45
|
+
|
46
|
+
# Restores GoogleSpreadsheet::Session from +path+ and returns it.
|
47
|
+
# If +path+ doesn't exist or authentication has failed, prompts mail and password on console,
|
48
|
+
# authenticates with them, stores the session to +path+ and returns it.
|
49
|
+
#
|
50
|
+
# This method requires Highline library: http://rubyforge.org/projects/highline/
|
51
|
+
def self.saved_session(path = ENV["HOME"] + "/.ruby_google_spreadsheet.token")
|
52
|
+
tokens = {}
|
53
|
+
if File.exist?(path)
|
54
|
+
open(path) do |f|
|
55
|
+
for auth in [:wise, :writely]
|
56
|
+
line = f.gets()
|
57
|
+
tokens[auth] = line && line.chomp()
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
session = Session.new(tokens)
|
62
|
+
session.on_auth_fail = proc() do
|
63
|
+
begin
|
64
|
+
require "highline"
|
65
|
+
rescue LoadError
|
66
|
+
raise(LoadError,
|
67
|
+
"GoogleSpreadsheet.saved_session requires Highline library.\n" +
|
68
|
+
"Run\n" +
|
69
|
+
" \$ sudo gem install highline\n" +
|
70
|
+
"to install it.")
|
71
|
+
end
|
72
|
+
highline = HighLine.new()
|
73
|
+
mail = highline.ask("Mail: ")
|
74
|
+
password = highline.ask("Password: "){ |q| q.echo = false }
|
75
|
+
session.login(mail, password)
|
76
|
+
open(path, "w", 0600) do |f|
|
77
|
+
f.puts(session.auth_token(:wise))
|
78
|
+
f.puts(session.auth_token(:writely))
|
79
|
+
end
|
80
|
+
true
|
81
|
+
end
|
82
|
+
if !session.auth_token
|
83
|
+
session.on_auth_fail.call()
|
84
|
+
end
|
85
|
+
return session
|
86
|
+
end
|
87
|
+
|
88
|
+
|
89
|
+
module Util #:nodoc:
|
90
|
+
|
91
|
+
module_function
|
92
|
+
|
93
|
+
def encode_query(params)
|
94
|
+
return params.map(){ |k, v| CGI.escape(k) + "=" + CGI.escape(v) }.join("&")
|
95
|
+
end
|
96
|
+
|
97
|
+
def h(str)
|
98
|
+
return CGI.escapeHTML(str.to_s())
|
99
|
+
end
|
100
|
+
|
101
|
+
def as_utf8(str)
|
102
|
+
if str.respond_to?(:force_encoding)
|
103
|
+
str.force_encoding("UTF-8")
|
104
|
+
else
|
105
|
+
str
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
end
|
110
|
+
|
111
|
+
|
112
|
+
# Raised when spreadsheets.google.com has returned error.
|
113
|
+
class Error < RuntimeError
|
114
|
+
|
115
|
+
end
|
116
|
+
|
117
|
+
|
118
|
+
# Raised when GoogleSpreadsheet.login has failed.
|
119
|
+
class AuthenticationError < GoogleSpreadsheet::Error
|
120
|
+
|
121
|
+
end
|
122
|
+
|
123
|
+
|
124
|
+
# Use GoogleSpreadsheet.login or GoogleSpreadsheet.saved_session to get
|
125
|
+
# GoogleSpreadsheet::Session object.
|
126
|
+
class Session
|
127
|
+
|
128
|
+
include(Util)
|
129
|
+
extend(Util)
|
130
|
+
|
131
|
+
# The same as GoogleSpreadsheet.login.
|
132
|
+
def self.login(mail, password)
|
133
|
+
session = Session.new()
|
134
|
+
session.login(mail, password)
|
135
|
+
return session
|
136
|
+
end
|
137
|
+
|
138
|
+
def self.authorized(oauth_token)
|
139
|
+
session = Session.new(nil, oauth_token)
|
140
|
+
end
|
141
|
+
|
142
|
+
# Restores session using return value of auth_tokens method of previous session.
|
143
|
+
# You can use oauth_token instead of gmail username and password if you have used Google hybrid protocol (Open ID+ OAuth)
|
144
|
+
def initialize(auth_tokens, oauth_token)
|
145
|
+
if oauth_token
|
146
|
+
@oauth_token = oauth_token
|
147
|
+
else
|
148
|
+
@auth_tokens = auth_tokens
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
# Authenticates with given +mail+ and +password+, and updates current session object
|
153
|
+
# if succeeds. Raises GoogleSpreadsheet::AuthenticationError if fails.
|
154
|
+
# Google Apps account is supported.
|
155
|
+
def login(mail, password)
|
156
|
+
begin
|
157
|
+
@auth_tokens = {}
|
158
|
+
authenticate(mail, password, :wise)
|
159
|
+
authenticate(mail, password, :writely)
|
160
|
+
rescue GoogleSpreadsheet::Error => ex
|
161
|
+
return true if @on_auth_fail && @on_auth_fail.call()
|
162
|
+
raise(AuthenticationError, "authentication failed for #{mail}: #{ex.message}")
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
# Authentication tokens.
|
167
|
+
attr_reader(:auth_tokens)
|
168
|
+
|
169
|
+
# Authentication token.
|
170
|
+
def auth_token(auth = :wise)
|
171
|
+
return @auth_tokens[auth]
|
172
|
+
end
|
173
|
+
|
174
|
+
# Proc or Method called when authentication has failed.
|
175
|
+
# When this function returns +true+, it tries again.
|
176
|
+
attr_accessor :on_auth_fail
|
177
|
+
|
178
|
+
def auth_header(auth) #:nodoc:
|
179
|
+
if auth == :none
|
180
|
+
return {}
|
181
|
+
else
|
182
|
+
return {"Authorization" => "GoogleLogin auth=#{@auth_tokens[auth]}"}
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
# Returns list of spreadsheets for the user as array of GoogleSpreadsheet::Spreadsheet.
|
187
|
+
# You can specify query parameters described at
|
188
|
+
# http://code.google.com/apis/spreadsheets/docs/2.0/reference.html#Parameters
|
189
|
+
#
|
190
|
+
# e.g.
|
191
|
+
# session.spreadsheets
|
192
|
+
# session.spreadsheets("title" => "hoge")
|
193
|
+
def spreadsheets(params = {})
|
194
|
+
query = encode_query(params)
|
195
|
+
doc = request(:get, "http://spreadsheets.google.com/feeds/spreadsheets/private/full?#{query}")
|
196
|
+
result = []
|
197
|
+
for entry in doc.search("entry")
|
198
|
+
title = as_utf8(entry.search("title").text)
|
199
|
+
url = as_utf8(entry.search(
|
200
|
+
"link[@rel='http://schemas.google.com/spreadsheets/2006#worksheetsfeed']")[0]["href"])
|
201
|
+
result.push(Spreadsheet.new(self, url, title))
|
202
|
+
end
|
203
|
+
return result
|
204
|
+
end
|
205
|
+
|
206
|
+
# Returns GoogleSpreadsheet::Spreadsheet with given +key+.
|
207
|
+
#
|
208
|
+
# e.g.
|
209
|
+
# # http://spreadsheets.google.com/ccc?key=pz7XtlQC-PYx-jrVMJErTcg&hl=ja
|
210
|
+
# session.spreadsheet_by_key("pz7XtlQC-PYx-jrVMJErTcg")
|
211
|
+
def spreadsheet_by_key(key)
|
212
|
+
url = "http://spreadsheets.google.com/feeds/worksheets/#{key}/private/full"
|
213
|
+
return Spreadsheet.new(self, url)
|
214
|
+
end
|
215
|
+
|
216
|
+
# Returns GoogleSpreadsheet::Spreadsheet with given +url+. You must specify either of:
|
217
|
+
# - URL of the page you open to access the spreadsheet in your browser
|
218
|
+
# - URL of worksheet-based feed of the spreadseet
|
219
|
+
#
|
220
|
+
# e.g.
|
221
|
+
# session.spreadsheet_by_url(
|
222
|
+
# "http://spreadsheets.google.com/ccc?key=pz7XtlQC-PYx-jrVMJErTcg&hl=en")
|
223
|
+
# session.spreadsheet_by_url(
|
224
|
+
# "http://spreadsheets.google.com/feeds/worksheets/pz7XtlQC-PYx-jrVMJErTcg/private/full")
|
225
|
+
def spreadsheet_by_url(url)
|
226
|
+
# Tries to parse it as URL of human-readable spreadsheet.
|
227
|
+
uri = URI.parse(url)
|
228
|
+
if uri.host == "spreadsheets.google.com" && uri.path =~ /\/ccc$/
|
229
|
+
if (uri.query || "").split(/&/).find(){ |s| s=~ /^key=(.*)$/ }
|
230
|
+
return spreadsheet_by_key($1)
|
231
|
+
end
|
232
|
+
end
|
233
|
+
# Assumes the URL is worksheets feed URL.
|
234
|
+
return Spreadsheet.new(self, url)
|
235
|
+
end
|
236
|
+
|
237
|
+
# Returns GoogleSpreadsheet::Worksheet with given +url+.
|
238
|
+
# You must specify URL of cell-based feed of the worksheet.
|
239
|
+
#
|
240
|
+
# e.g.
|
241
|
+
# session.worksheet_by_url(
|
242
|
+
# "http://spreadsheets.google.com/feeds/cells/pz7XtlQC-PYxNmbBVgyiNWg/od6/private/full")
|
243
|
+
def worksheet_by_url(url)
|
244
|
+
return Worksheet.new(self, nil, url)
|
245
|
+
end
|
246
|
+
|
247
|
+
# Creates new spreadsheet and returns the new GoogleSpreadsheet::Spreadsheet.
|
248
|
+
#
|
249
|
+
# e.g.
|
250
|
+
# session.create_spreadsheet("My new sheet")
|
251
|
+
def create_spreadsheet(
|
252
|
+
title = "Untitled",
|
253
|
+
feed_url = "http://docs.google.com/feeds/documents/private/full")
|
254
|
+
xml = <<-"EOS"
|
255
|
+
<atom:entry xmlns:atom="http://www.w3.org/2005/Atom" xmlns:docs="http://schemas.google.com/docs/2007">
|
256
|
+
<atom:category scheme="http://schemas.google.com/g/2005#kind"
|
257
|
+
term="http://schemas.google.com/docs/2007#spreadsheet" label="spreadsheet"/>
|
258
|
+
<atom:title>#{h(title)}</atom:title>
|
259
|
+
</atom:entry>
|
260
|
+
EOS
|
261
|
+
|
262
|
+
doc = request(:post, feed_url, :data => xml, :auth => :writely)
|
263
|
+
ss_url = as_utf8(doc.search(
|
264
|
+
"link[@rel='http://schemas.google.com/spreadsheets/2006#worksheetsfeed']")[0]["href"])
|
265
|
+
return Spreadsheet.new(self, ss_url, title)
|
266
|
+
end
|
267
|
+
|
268
|
+
def request(method, url, params = {}) #:nodoc:
|
269
|
+
if @oauth_token
|
270
|
+
if method == :get
|
271
|
+
response = @oauth_token.get(url)
|
272
|
+
return Hpricot.XML(response.body)
|
273
|
+
elsif method == :post
|
274
|
+
response = @oauth_token.post(url,params[:data])
|
275
|
+
return Hpricot.XML(response.body)
|
276
|
+
end
|
277
|
+
else
|
278
|
+
uri = URI.parse(url)
|
279
|
+
data = params[:data]
|
280
|
+
auth = params[:auth] || :wise
|
281
|
+
if params[:header]
|
282
|
+
add_header = params[:header]
|
283
|
+
else
|
284
|
+
add_header = data ? {"Content-Type" => "application/atom+xml"} : {}
|
285
|
+
end
|
286
|
+
response_type = params[:response_type] || :xml
|
287
|
+
|
288
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
289
|
+
http.use_ssl = uri.scheme == "https"
|
290
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
291
|
+
http.start() do
|
292
|
+
while true
|
293
|
+
path = uri.path + (uri.query ? "?#{uri.query}" : "")
|
294
|
+
header = auth_header(auth).merge(add_header)
|
295
|
+
if method == :delete || method == :get
|
296
|
+
response = http.__send__(method, path, header)
|
297
|
+
else
|
298
|
+
response = http.__send__(method, path, data, header)
|
299
|
+
end
|
300
|
+
if response.code == "401" && @on_auth_fail && @on_auth_fail.call()
|
301
|
+
next
|
302
|
+
end
|
303
|
+
if !(response.code =~ /^2/)
|
304
|
+
raise(
|
305
|
+
response.code == "401" ? AuthenticationError : GoogleSpreadsheet::Error,
|
306
|
+
"Response code #{response.code} for #{method} #{url}: " +
|
307
|
+
CGI.unescapeHTML(response.body))
|
308
|
+
end
|
309
|
+
case response_type
|
310
|
+
when :xml
|
311
|
+
return Hpricot.XML(response.body)
|
312
|
+
when :raw
|
313
|
+
return response.body
|
314
|
+
else
|
315
|
+
raise("unknown params[:response_type]: %s" % response_type)
|
316
|
+
end
|
317
|
+
end
|
318
|
+
end
|
319
|
+
end
|
320
|
+
end
|
321
|
+
|
322
|
+
private
|
323
|
+
|
324
|
+
def authenticate(mail, password, auth)
|
325
|
+
params = {
|
326
|
+
"accountType" => "HOSTED_OR_GOOGLE",
|
327
|
+
"Email" => mail,
|
328
|
+
"Passwd" => password,
|
329
|
+
"service" => auth.to_s(),
|
330
|
+
"source" => "Gimite-RubyGoogleSpreadsheet-1.00",
|
331
|
+
}
|
332
|
+
response = request(:post,
|
333
|
+
"https://www.google.com/accounts/ClientLogin",
|
334
|
+
:data => encode_query(params), :auth => :none, :header => {}, :response_type => :raw)
|
335
|
+
@auth_tokens[auth] = response.slice(/^Auth=(.*)$/, 1)
|
336
|
+
end
|
337
|
+
|
338
|
+
end
|
339
|
+
|
340
|
+
|
341
|
+
# Use methods in GoogleSpreadsheet::Session to get GoogleSpreadsheet::Spreadsheet object.
|
342
|
+
class Spreadsheet
|
343
|
+
|
344
|
+
include(Util)
|
345
|
+
|
346
|
+
def initialize(session, worksheets_feed_url, title = nil) #:nodoc:
|
347
|
+
@session = session
|
348
|
+
@worksheets_feed_url = worksheets_feed_url
|
349
|
+
@title = title
|
350
|
+
end
|
351
|
+
|
352
|
+
# URL of worksheet-based feed of the spreadsheet.
|
353
|
+
attr_reader(:worksheets_feed_url)
|
354
|
+
|
355
|
+
# Title of the spreadsheet. So far only available if you get this object by
|
356
|
+
# GoogleSpreadsheet::Session#spreadsheets.
|
357
|
+
attr_reader(:title)
|
358
|
+
|
359
|
+
# Key of the spreadsheet.
|
360
|
+
def key
|
361
|
+
if !(@worksheets_feed_url =~
|
362
|
+
%r{http://spreadsheets.google.com/feeds/worksheets/(.*)/private/full})
|
363
|
+
raise(GoogleSpreadsheet::Error,
|
364
|
+
"worksheets feed URL is in unknown format: #{@worksheets_feed_url}")
|
365
|
+
end
|
366
|
+
return $1
|
367
|
+
end
|
368
|
+
|
369
|
+
# Tables feed URL of the spreadsheet.
|
370
|
+
def tables_feed_url
|
371
|
+
return "http://spreadsheets.google.com/feeds/#{self.key}/tables"
|
372
|
+
end
|
373
|
+
|
374
|
+
# URL of feed used in document list feed API.
|
375
|
+
def document_feed_url
|
376
|
+
return "http://docs.google.com/feeds/documents/private/full/spreadsheet%3A#{self.key}"
|
377
|
+
end
|
378
|
+
|
379
|
+
# Creates copy of this spreadsheet with the given name.
|
380
|
+
def duplicate(new_name = nil)
|
381
|
+
new_name ||= (@title ? "Copy of " + @title : "Untitled")
|
382
|
+
get_url = "http://spreadsheets.google.com/feeds/download/spreadsheets/Export?key=#{key}&exportFormat=ods"
|
383
|
+
ods = @session.request(:get, get_url, :response_type => :raw)
|
384
|
+
|
385
|
+
url = "http://docs.google.com/feeds/documents/private/full"
|
386
|
+
header = {
|
387
|
+
"Content-Type" => "application/x-vnd.oasis.opendocument.spreadsheet",
|
388
|
+
"Slug" => URI.encode(new_name),
|
389
|
+
}
|
390
|
+
doc = @session.request(:post, url, :data => ods, :auth => :writely, :header => header)
|
391
|
+
ss_url = as_utf8(doc.search(
|
392
|
+
"link[@rel='http://schemas.google.com/spreadsheets/2006#worksheetsfeed']")[0]["href"])
|
393
|
+
return Spreadsheet.new(@session, ss_url, title)
|
394
|
+
end
|
395
|
+
|
396
|
+
# If +permanent+ is +false+, moves the spreadsheet to the trash.
|
397
|
+
# If +permanent+ is +true+, deletes the spreadsheet permanently.
|
398
|
+
def delete(permanent = false)
|
399
|
+
@session.request(:delete,
|
400
|
+
self.document_feed_url + (permanent ? "?delete=true" : ""),
|
401
|
+
:auth => :writely, :header => {"If-Match" => "*"})
|
402
|
+
end
|
403
|
+
|
404
|
+
# Returns worksheets of the spreadsheet as array of GoogleSpreadsheet::Worksheet.
|
405
|
+
def worksheets
|
406
|
+
doc = @session.request(:get, @worksheets_feed_url)
|
407
|
+
result = []
|
408
|
+
for entry in doc.search("entry")
|
409
|
+
title = as_utf8(entry.search("title").text)
|
410
|
+
url = as_utf8(entry.search(
|
411
|
+
"link[@rel='http://schemas.google.com/spreadsheets/2006#cellsfeed']")[0]["href"])
|
412
|
+
result.push(Worksheet.new(@session, self, url, title))
|
413
|
+
end
|
414
|
+
return result.freeze()
|
415
|
+
end
|
416
|
+
|
417
|
+
# Adds a new worksheet to the spreadsheet. Returns added GoogleSpreadsheet::Worksheet.
|
418
|
+
def add_worksheet(title, max_rows = 100, max_cols = 20)
|
419
|
+
xml = <<-"EOS"
|
420
|
+
<entry xmlns='http://www.w3.org/2005/Atom'
|
421
|
+
xmlns:gs='http://schemas.google.com/spreadsheets/2006'>
|
422
|
+
<title>#{h(title)}</title>
|
423
|
+
<gs:rowCount>#{h(max_rows)}</gs:rowCount>
|
424
|
+
<gs:colCount>#{h(max_cols)}</gs:colCount>
|
425
|
+
</entry>
|
426
|
+
EOS
|
427
|
+
doc = @session.request(:post, @worksheets_feed_url, :data => xml)
|
428
|
+
url = as_utf8(doc.search(
|
429
|
+
"link[@rel='http://schemas.google.com/spreadsheets/2006#cellsfeed']")[0]["href"])
|
430
|
+
return Worksheet.new(@session, self, url, title)
|
431
|
+
end
|
432
|
+
|
433
|
+
# Returns list of tables in the spreadsheet.
|
434
|
+
def tables
|
435
|
+
doc = @session.request(:get, self.tables_feed_url)
|
436
|
+
return doc.search("entry").map(){ |e| Table.new(@session, e) }.freeze()
|
437
|
+
end
|
438
|
+
|
439
|
+
end
|
440
|
+
|
441
|
+
# Use GoogleSpreadsheet::Worksheet#add_table to create table.
|
442
|
+
# Use GoogleSpreadsheet::Worksheet#tables to get GoogleSpreadsheet::Table objects.
|
443
|
+
class Table
|
444
|
+
|
445
|
+
include(Util)
|
446
|
+
|
447
|
+
def initialize(session, entry) #:nodoc:
|
448
|
+
@columns = {}
|
449
|
+
@worksheet_title = as_utf8(entry.search("gs:worksheet")[0]["name"])
|
450
|
+
@records_url = as_utf8(entry.search("content")[0]["src"])
|
451
|
+
@session = session
|
452
|
+
end
|
453
|
+
|
454
|
+
# Title of the worksheet the table belongs to.
|
455
|
+
attr_reader(:worksheet_title)
|
456
|
+
|
457
|
+
# Adds a record.
|
458
|
+
def add_record(values)
|
459
|
+
fields = ""
|
460
|
+
values.each do |name, value|
|
461
|
+
fields += "<gs:field name='#{h(name)}'>#{h(value)}</gs:field>"
|
462
|
+
end
|
463
|
+
xml =<<-EOS
|
464
|
+
<entry
|
465
|
+
xmlns="http://www.w3.org/2005/Atom"
|
466
|
+
xmlns:gs="http://schemas.google.com/spreadsheets/2006">
|
467
|
+
#{fields}
|
468
|
+
</entry>
|
469
|
+
EOS
|
470
|
+
@session.request(:post, @records_url, :data => xml)
|
471
|
+
end
|
472
|
+
|
473
|
+
# Returns records in the table.
|
474
|
+
def records
|
475
|
+
doc = @session.request(:get, @records_url)
|
476
|
+
return doc.search("entry").map(){ |e| Record.new(@session, e) }
|
477
|
+
end
|
478
|
+
|
479
|
+
end
|
480
|
+
|
481
|
+
# Use GoogleSpreadsheet::Table#records to get GoogleSpreadsheet::Record objects.
|
482
|
+
class Record < Hash
|
483
|
+
|
484
|
+
def initialize(session, entry) #:nodoc:
|
485
|
+
@session = session
|
486
|
+
for field in entry.search("gs:field")
|
487
|
+
self[as_utf8(field["name"])] = as_utf8(field.inner_text)
|
488
|
+
end
|
489
|
+
end
|
490
|
+
|
491
|
+
def inspect #:nodoc:
|
492
|
+
content = self.map(){ |k, v| "%p => %p" % [k, v] }.join(", ")
|
493
|
+
return "\#<%p:{%s}>" % [self.class, content]
|
494
|
+
end
|
495
|
+
|
496
|
+
end
|
497
|
+
|
498
|
+
# Use GoogleSpreadsheet::Spreadsheet#worksheets to get GoogleSpreadsheet::Worksheet object.
|
499
|
+
class Worksheet
|
500
|
+
|
501
|
+
include(Util)
|
502
|
+
|
503
|
+
def initialize(session, spreadsheet, cells_feed_url, title = nil) #:nodoc:
|
504
|
+
@session = session
|
505
|
+
@spreadsheet = spreadsheet
|
506
|
+
@cells_feed_url = cells_feed_url
|
507
|
+
@title = title
|
508
|
+
|
509
|
+
@cells = nil
|
510
|
+
@input_values = nil
|
511
|
+
@modified = Set.new()
|
512
|
+
end
|
513
|
+
|
514
|
+
# URL of cell-based feed of the worksheet.
|
515
|
+
attr_reader(:cells_feed_url)
|
516
|
+
|
517
|
+
# URL of worksheet feed URL of the worksheet.
|
518
|
+
def worksheet_feed_url
|
519
|
+
# I don't know good way to get worksheet feed URL from cells feed URL.
|
520
|
+
# Probably it would be cleaner to keep worksheet feed URL and get cells feed URL
|
521
|
+
# from it.
|
522
|
+
if !(@cells_feed_url =~
|
523
|
+
%r{^http://spreadsheets.google.com/feeds/cells/(.*)/(.*)/private/full$})
|
524
|
+
raise(GoogleSpreadsheet::Error,
|
525
|
+
"cells feed URL is in unknown format: #{@cells_feed_url}")
|
526
|
+
end
|
527
|
+
return "http://spreadsheets.google.com/feeds/worksheets/#{$1}/private/full/#{$2}"
|
528
|
+
end
|
529
|
+
|
530
|
+
# GoogleSpreadsheet::Spreadsheet which this worksheet belongs to.
|
531
|
+
def spreadsheet
|
532
|
+
if !@spreadsheet
|
533
|
+
if !(@cells_feed_url =~
|
534
|
+
%r{^http://spreadsheets.google.com/feeds/cells/(.*)/(.*)/private/full$})
|
535
|
+
raise(GoogleSpreadsheet::Error,
|
536
|
+
"cells feed URL is in unknown format: #{@cells_feed_url}")
|
537
|
+
end
|
538
|
+
@spreadsheet = @session.spreadsheet_by_key($1)
|
539
|
+
end
|
540
|
+
return @spreadsheet
|
541
|
+
end
|
542
|
+
|
543
|
+
# Returns content of the cell as String. Top-left cell is [1, 1].
|
544
|
+
def [](row, col)
|
545
|
+
return self.cells[[row, col]] || ""
|
546
|
+
end
|
547
|
+
|
548
|
+
# Updates content of the cell.
|
549
|
+
# Note that update is not sent to the server until you call save().
|
550
|
+
# Top-left cell is [1, 1].
|
551
|
+
#
|
552
|
+
# e.g.
|
553
|
+
# worksheet[2, 1] = "hoge"
|
554
|
+
# worksheet[1, 3] = "=A1+B1"
|
555
|
+
def []=(row, col, value)
|
556
|
+
reload() if !@cells
|
557
|
+
@cells[[row, col]] = value
|
558
|
+
@input_values[[row, col]] = value
|
559
|
+
@modified.add([row, col])
|
560
|
+
self.max_rows = row if row > @max_rows
|
561
|
+
self.max_cols = col if col > @max_cols
|
562
|
+
end
|
563
|
+
|
564
|
+
# Returns the value or the formula of the cell. Top-left cell is [1, 1].
|
565
|
+
#
|
566
|
+
# If user input "=A1+B1" to cell [1, 3], worksheet[1, 3] is "3" for example and
|
567
|
+
# worksheet.input_value(1, 3) is "=RC[-2]+RC[-1]".
|
568
|
+
def input_value(row, col)
|
569
|
+
reload() if !@cells
|
570
|
+
return @input_values[[row, col]] || ""
|
571
|
+
end
|
572
|
+
|
573
|
+
# Row number of the bottom-most non-empty row.
|
574
|
+
def num_rows
|
575
|
+
reload() if !@cells
|
576
|
+
return @cells.keys.map(){ |r, c| r }.max || 0
|
577
|
+
end
|
578
|
+
|
579
|
+
# Column number of the right-most non-empty column.
|
580
|
+
def num_cols
|
581
|
+
reload() if !@cells
|
582
|
+
return @cells.keys.map(){ |r, c| c }.max || 0
|
583
|
+
end
|
584
|
+
|
585
|
+
# Number of rows including empty rows.
|
586
|
+
def max_rows
|
587
|
+
reload() if !@cells
|
588
|
+
return @max_rows
|
589
|
+
end
|
590
|
+
|
591
|
+
# Updates number of rows.
|
592
|
+
# Note that update is not sent to the server until you call save().
|
593
|
+
def max_rows=(rows)
|
594
|
+
reload() if !@cells
|
595
|
+
@max_rows = rows
|
596
|
+
@meta_modified = true
|
597
|
+
end
|
598
|
+
|
599
|
+
# Number of columns including empty columns.
|
600
|
+
def max_cols
|
601
|
+
reload() if !@cells
|
602
|
+
return @max_cols
|
603
|
+
end
|
604
|
+
|
605
|
+
# Updates number of columns.
|
606
|
+
# Note that update is not sent to the server until you call save().
|
607
|
+
def max_cols=(cols)
|
608
|
+
reload() if !@cells
|
609
|
+
@max_cols = cols
|
610
|
+
@meta_modified = true
|
611
|
+
end
|
612
|
+
|
613
|
+
# Title of the worksheet (shown as tab label in Web interface).
|
614
|
+
def title
|
615
|
+
reload() if !@title
|
616
|
+
return @title
|
617
|
+
end
|
618
|
+
|
619
|
+
# Updates title of the worksheet.
|
620
|
+
# Note that update is not sent to the server until you call save().
|
621
|
+
def title=(title)
|
622
|
+
reload() if !@cells
|
623
|
+
@title = title
|
624
|
+
@meta_modified = true
|
625
|
+
end
|
626
|
+
|
627
|
+
def cells #:nodoc:
|
628
|
+
reload() if !@cells
|
629
|
+
return @cells
|
630
|
+
end
|
631
|
+
|
632
|
+
# An array of spreadsheet rows. Each row contains an array of
|
633
|
+
# columns. Note that resulting array is 0-origin so
|
634
|
+
# worksheet.rows[0][0] == worksheet[1, 1].
|
635
|
+
def rows(skip = 0)
|
636
|
+
nc = self.num_cols
|
637
|
+
result = ((1 + skip)..self.num_rows).map() do |row|
|
638
|
+
(1..nc).map(){ |col| self[row, col] }.freeze()
|
639
|
+
end
|
640
|
+
return result.freeze()
|
641
|
+
end
|
642
|
+
|
643
|
+
# Reloads content of the worksheets from the server.
|
644
|
+
# Note that changes you made by []= is discarded if you haven't called save().
|
645
|
+
def reload()
|
646
|
+
doc = @session.request(:get, @cells_feed_url)
|
647
|
+
@max_rows = doc.search("gs:rowCount").text.to_i()
|
648
|
+
@max_cols = doc.search("gs:colCount").text.to_i()
|
649
|
+
@title = as_utf8(doc.search("/feed/title").text)
|
650
|
+
|
651
|
+
@cells = {}
|
652
|
+
@input_values = {}
|
653
|
+
for entry in doc.search("entry")
|
654
|
+
cell = entry.search("gs:cell")[0]
|
655
|
+
row = cell["row"].to_i()
|
656
|
+
col = cell["col"].to_i()
|
657
|
+
@cells[[row, col]] = as_utf8(cell.inner_text)
|
658
|
+
@input_values[[row, col]] = as_utf8(cell["inputValue"])
|
659
|
+
end
|
660
|
+
@modified.clear()
|
661
|
+
@meta_modified = false
|
662
|
+
return true
|
663
|
+
end
|
664
|
+
|
665
|
+
# Saves your changes made by []=, etc. to the server.
|
666
|
+
def save()
|
667
|
+
sent = false
|
668
|
+
|
669
|
+
if @meta_modified
|
670
|
+
|
671
|
+
ws_doc = @session.request(:get, self.worksheet_feed_url)
|
672
|
+
edit_url = ws_doc.search("link[@rel='edit']")[0]["href"]
|
673
|
+
xml = <<-"EOS"
|
674
|
+
<entry xmlns='http://www.w3.org/2005/Atom'
|
675
|
+
xmlns:gs='http://schemas.google.com/spreadsheets/2006'>
|
676
|
+
<title>#{h(self.title)}</title>
|
677
|
+
<gs:rowCount>#{h(self.max_rows)}</gs:rowCount>
|
678
|
+
<gs:colCount>#{h(self.max_cols)}</gs:colCount>
|
679
|
+
</entry>
|
680
|
+
EOS
|
681
|
+
|
682
|
+
@session.request(:put, edit_url, :data => xml)
|
683
|
+
|
684
|
+
@meta_modified = false
|
685
|
+
sent = true
|
686
|
+
|
687
|
+
end
|
688
|
+
|
689
|
+
if !@modified.empty?
|
690
|
+
|
691
|
+
# Gets id and edit URL for each cell.
|
692
|
+
# Note that return-empty=true is required to get those info for empty cells.
|
693
|
+
cell_entries = {}
|
694
|
+
rows = @modified.map(){ |r, c| r }
|
695
|
+
cols = @modified.map(){ |r, c| c }
|
696
|
+
url = "#{@cells_feed_url}?return-empty=true&min-row=#{rows.min}&max-row=#{rows.max}" +
|
697
|
+
"&min-col=#{cols.min}&max-col=#{cols.max}"
|
698
|
+
doc = @session.request(:get, url)
|
699
|
+
for entry in doc.search("entry")
|
700
|
+
row = entry.search("gs:cell")[0]["row"].to_i()
|
701
|
+
col = entry.search("gs:cell")[0]["col"].to_i()
|
702
|
+
cell_entries[[row, col]] = entry
|
703
|
+
end
|
704
|
+
|
705
|
+
# Updates cell values using batch operation.
|
706
|
+
# If the data is large, we split it into multiple operations, otherwise batch may fail.
|
707
|
+
@modified.each_slice(250) do |chunk|
|
708
|
+
|
709
|
+
xml = <<-EOS
|
710
|
+
<feed xmlns="http://www.w3.org/2005/Atom"
|
711
|
+
xmlns:batch="http://schemas.google.com/gdata/batch"
|
712
|
+
xmlns:gs="http://schemas.google.com/spreadsheets/2006">
|
713
|
+
<id>#{h(@cells_feed_url)}</id>
|
714
|
+
EOS
|
715
|
+
for row, col in chunk
|
716
|
+
value = @cells[[row, col]]
|
717
|
+
entry = cell_entries[[row, col]]
|
718
|
+
id = entry.search("id").text
|
719
|
+
edit_url = entry.search("link[@rel='edit']")[0]["href"]
|
720
|
+
xml << <<-EOS
|
721
|
+
<entry>
|
722
|
+
<batch:id>#{h(row)},#{h(col)}</batch:id>
|
723
|
+
<batch:operation type="update"/>
|
724
|
+
<id>#{h(id)}</id>
|
725
|
+
<link rel="edit" type="application/atom+xml"
|
726
|
+
href="#{h(edit_url)}"/>
|
727
|
+
<gs:cell row="#{h(row)}" col="#{h(col)}" inputValue="#{h(value)}"/>
|
728
|
+
</entry>
|
729
|
+
EOS
|
730
|
+
end
|
731
|
+
xml << <<-"EOS"
|
732
|
+
</feed>
|
733
|
+
EOS
|
734
|
+
|
735
|
+
result = @session.request(:post, "#{@cells_feed_url}/batch", :data => xml)
|
736
|
+
for entry in result.search("atom:entry")
|
737
|
+
interrupted = entry.search("batch:interrupted")[0]
|
738
|
+
if interrupted
|
739
|
+
raise(GoogleSpreadsheet::Error, "Update has failed: %s" %
|
740
|
+
interrupted["reason"])
|
741
|
+
end
|
742
|
+
if !(entry.search("batch:status")[0]["code"] =~ /^2/)
|
743
|
+
raise(GoogleSpreadsheet::Error, "Updating cell %s has failed: %s" %
|
744
|
+
[entry.search("atom:id").text, entry.search("batch:status")[0]["reason"]])
|
745
|
+
end
|
746
|
+
end
|
747
|
+
|
748
|
+
end
|
749
|
+
|
750
|
+
@modified.clear()
|
751
|
+
sent = true
|
752
|
+
|
753
|
+
end
|
754
|
+
return sent
|
755
|
+
end
|
756
|
+
|
757
|
+
# Calls save() and reload().
|
758
|
+
def synchronize()
|
759
|
+
save()
|
760
|
+
reload()
|
761
|
+
end
|
762
|
+
|
763
|
+
# Deletes this worksheet. Deletion takes effect right away without calling save().
|
764
|
+
def delete()
|
765
|
+
ws_doc = @session.request(:get, self.worksheet_feed_url)
|
766
|
+
edit_url = ws_doc.search("link[@rel='edit']")[0]["href"]
|
767
|
+
@session.request(:delete, edit_url)
|
768
|
+
end
|
769
|
+
|
770
|
+
# Returns true if you have changes made by []= which haven't been saved.
|
771
|
+
def dirty?
|
772
|
+
return !@modified.empty?
|
773
|
+
end
|
774
|
+
|
775
|
+
# Creates table for the worksheet and returns GoogleSpreadsheet::Table.
|
776
|
+
# See this document for details:
|
777
|
+
# http://code.google.com/intl/en/apis/spreadsheets/docs/3.0/developers_guide_protocol.html#TableFeeds
|
778
|
+
def add_table(table_title, summary, columns)
|
779
|
+
column_xml = ""
|
780
|
+
columns.each do |index, name|
|
781
|
+
column_xml += "<gs:column index='#{h(index)}' name='#{h(name)}'/>\n"
|
782
|
+
end
|
783
|
+
|
784
|
+
xml = <<-"EOS"
|
785
|
+
<entry xmlns="http://www.w3.org/2005/Atom"
|
786
|
+
xmlns:gs="http://schemas.google.com/spreadsheets/2006">
|
787
|
+
<title type='text'>#{h(table_title)}</title>
|
788
|
+
<summary type='text'>#{h(summary)}</summary>
|
789
|
+
<gs:worksheet name='#{h(self.title)}' />
|
790
|
+
<gs:header row='1' />
|
791
|
+
<gs:data numRows='0' startRow='2'>
|
792
|
+
#{column_xml}
|
793
|
+
</gs:data>
|
794
|
+
</entry>
|
795
|
+
EOS
|
796
|
+
|
797
|
+
result = @session.request(:post, self.spreadsheet.tables_feed_url, :data => xml)
|
798
|
+
return Table.new(@session, result)
|
799
|
+
end
|
800
|
+
|
801
|
+
# Returns list of tables for the workwheet.
|
802
|
+
def tables
|
803
|
+
return self.spreadsheet.tables.select(){ |t| t.worksheet_title == self.title }
|
804
|
+
end
|
805
|
+
|
806
|
+
end
|
807
|
+
|
808
|
+
|
809
|
+
end
|
metadata
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: sprout-google-spreadsheet-ruby
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- sprout
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2010-01-12 00:00:00 +05:45
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: hpricot
|
17
|
+
type: :development
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: "0.3"
|
24
|
+
version:
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: oauth
|
27
|
+
type: :development
|
28
|
+
version_requirement:
|
29
|
+
version_requirements: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 0.3.6
|
34
|
+
version:
|
35
|
+
description: This is a library to read/write Google Spreadsheet using oauth and client login.
|
36
|
+
email:
|
37
|
+
- sprout+github@gmail.com
|
38
|
+
executables: []
|
39
|
+
|
40
|
+
extensions: []
|
41
|
+
|
42
|
+
extra_rdoc_files:
|
43
|
+
- README.rdoc
|
44
|
+
files:
|
45
|
+
- README.rdoc
|
46
|
+
- lib/google_spreadsheet.rb
|
47
|
+
has_rdoc: true
|
48
|
+
homepage: http://github.com/sprout/google-spreadsheet-ruby/tree/master
|
49
|
+
licenses: []
|
50
|
+
|
51
|
+
post_install_message:
|
52
|
+
rdoc_options:
|
53
|
+
- --main
|
54
|
+
- README.rdoc
|
55
|
+
require_paths:
|
56
|
+
- lib
|
57
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: "0"
|
62
|
+
version:
|
63
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - ">="
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: "0"
|
68
|
+
version:
|
69
|
+
requirements: []
|
70
|
+
|
71
|
+
rubyforge_project:
|
72
|
+
rubygems_version: 1.3.5
|
73
|
+
signing_key:
|
74
|
+
specification_version: 2
|
75
|
+
summary: This is a library to read/write Google Spreadsheet using oauth and client login.
|
76
|
+
test_files: []
|
77
|
+
|