google-spreadsheet-ruby 0.1.8 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README.rdoc +6 -3
- data/doc_src/google_spreadsheet/acl.rb +20 -0
- data/doc_src/google_spreadsheet/acl_entry.rb +33 -0
- data/lib/google_spreadsheet.rb +8 -1237
- data/lib/google_spreadsheet/acl.rb +124 -0
- data/lib/google_spreadsheet/acl_entry.rb +58 -0
- data/lib/google_spreadsheet/authentication_error.rb +14 -0
- data/lib/google_spreadsheet/client_login_fetcher.rb +56 -0
- data/lib/google_spreadsheet/collection.rb +62 -0
- data/lib/google_spreadsheet/error.rb +12 -0
- data/lib/google_spreadsheet/list.rb +115 -0
- data/lib/google_spreadsheet/list_row.rb +84 -0
- data/lib/google_spreadsheet/oauth1_fetcher.rb +26 -0
- data/lib/google_spreadsheet/oauth2_fetcher.rb +29 -0
- data/lib/google_spreadsheet/record.rb +31 -0
- data/lib/google_spreadsheet/session.rb +297 -0
- data/lib/google_spreadsheet/spreadsheet.rb +298 -0
- data/lib/google_spreadsheet/table.rb +60 -0
- data/lib/google_spreadsheet/util.rb +30 -0
- data/lib/google_spreadsheet/worksheet.rb +445 -0
- metadata +46 -9
data/README.rdoc
CHANGED
@@ -16,16 +16,19 @@ Example:
|
|
16
16
|
require "google_spreadsheet"
|
17
17
|
|
18
18
|
# Logs in.
|
19
|
-
# You can also use OAuth. See document of
|
19
|
+
# You can also use OAuth. See document of
|
20
|
+
# GoogleSpreadsheet.login_with_oauth for details.
|
20
21
|
session = GoogleSpreadsheet.login("username@gmail.com", "mypassword")
|
21
22
|
|
22
|
-
# First worksheet of
|
23
|
+
# First worksheet of
|
24
|
+
# https://docs.google.com/spreadsheet/ccc?key=pz7XtlQC-PYx-jrVMJErTcg
|
23
25
|
ws = session.spreadsheet_by_key("pz7XtlQC-PYx-jrVMJErTcg").worksheets[0]
|
24
26
|
|
25
27
|
# Gets content of A2 cell.
|
26
28
|
p ws[2, 1] #==> "hoge"
|
27
29
|
|
28
|
-
# Changes content of cells.
|
30
|
+
# Changes content of cells.
|
31
|
+
# Changes are not sent to the server until you call ws.save().
|
29
32
|
ws[2, 1] = "foo"
|
30
33
|
ws[2, 2] = "bar"
|
31
34
|
ws.save()
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module GoogleSpreadsheet
|
2
|
+
|
3
|
+
class Acl
|
4
|
+
|
5
|
+
# Returns the number of entries.
|
6
|
+
def size
|
7
|
+
end
|
8
|
+
|
9
|
+
# Returns GoogleSpreadsheet::AclEntry object at +index+.
|
10
|
+
def [](index)
|
11
|
+
end
|
12
|
+
|
13
|
+
# Iterates over GoogleSpreadsheet::AclEntry objects.
|
14
|
+
def each(&block)
|
15
|
+
yield(entry)
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module GoogleSpreadsheet
|
2
|
+
|
3
|
+
class AclEntry
|
4
|
+
|
5
|
+
# Type of the scope. One of:
|
6
|
+
#
|
7
|
+
# - "user": scope is a user's email address.
|
8
|
+
# - "group": scope is a Google Group email address.
|
9
|
+
# - "domain": scope is a Google Apps domain.
|
10
|
+
# - "default": Publicly shared with all users. scope is +nil+.
|
11
|
+
attr_reader(:scope_type)
|
12
|
+
|
13
|
+
# The scope. See scope_type.
|
14
|
+
attr_reader(:scope)
|
15
|
+
|
16
|
+
# The role given to the scope. One of:
|
17
|
+
# - "owner": The owner.
|
18
|
+
# - "writer": With read/write access.
|
19
|
+
# - "reader": With read-only access.
|
20
|
+
attr_reader(:role)
|
21
|
+
|
22
|
+
# Title of the entry.
|
23
|
+
attr_reader(:title)
|
24
|
+
|
25
|
+
# Edit URL of the entry.
|
26
|
+
attr_reader(:edit_url)
|
27
|
+
|
28
|
+
# E-tag of the entry.
|
29
|
+
attr_reader(:etag)
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
data/lib/google_spreadsheet.rb
CHANGED
@@ -1,17 +1,8 @@
|
|
1
1
|
# Author: Hiroshi Ichikawa <http://gimite.net/>
|
2
2
|
# The license of this source is "New BSD Licence"
|
3
3
|
|
4
|
-
require "
|
5
|
-
|
6
|
-
require "net/https"
|
7
|
-
require "open-uri"
|
8
|
-
require "cgi"
|
9
|
-
require "uri"
|
10
|
-
require "rubygems"
|
11
|
-
require "nokogiri"
|
12
|
-
require "oauth"
|
13
|
-
require "oauth2"
|
14
|
-
Net::HTTP.version_1_2
|
4
|
+
require "google_spreadsheet/session"
|
5
|
+
|
15
6
|
|
16
7
|
module GoogleSpreadsheet
|
17
8
|
|
@@ -53,15 +44,17 @@ module GoogleSpreadsheet
|
|
53
44
|
#
|
54
45
|
# OAuth1 code example:
|
55
46
|
#
|
56
|
-
# 1) First generate OAuth consumer object with key and secret for your site by registering site
|
47
|
+
# 1) First generate OAuth consumer object with key and secret for your site by registering site
|
48
|
+
# with Google.
|
57
49
|
# @consumer = OAuth::Consumer.new( "key","secret", {:site=>"https://agree2"})
|
58
|
-
# 2) Request token with OAuth
|
50
|
+
# 2) Request token with OAuth.
|
59
51
|
# @request_token = @consumer.get_request_token
|
60
52
|
# session[:request_token] = @request_token
|
61
53
|
# redirect_to @request_token.authorize_url
|
62
|
-
# 3) Create an oauth access token
|
54
|
+
# 3) Create an oauth access token.
|
63
55
|
# @oauth_access_token = @request_token.get_access_token
|
64
|
-
# @access_token = OAuth::AccessToken.new(
|
56
|
+
# @access_token = OAuth::AccessToken.new(
|
57
|
+
# @consumer, @oauth_access_token.token, @oauth_access_token.secret)
|
65
58
|
#
|
66
59
|
# See these documents for details:
|
67
60
|
#
|
@@ -123,1227 +116,5 @@ module GoogleSpreadsheet
|
|
123
116
|
end
|
124
117
|
return session
|
125
118
|
end
|
126
|
-
|
127
|
-
|
128
|
-
module Util #:nodoc:
|
129
|
-
|
130
|
-
module_function
|
131
|
-
|
132
|
-
def encode_query(params)
|
133
|
-
return params.map(){ |k, v| CGI.escape(k) + "=" + CGI.escape(v) }.join("&")
|
134
|
-
end
|
135
|
-
|
136
|
-
def concat_url(url, piece)
|
137
|
-
(url_base, url_query) = url.split(/\?/, 2)
|
138
|
-
(piece_base, piece_query) = piece.split(/\?/, 2)
|
139
|
-
result_query = [url_query, piece_query].select(){ |s| s && !s.empty? }.join("&")
|
140
|
-
return url_base + piece_base + (result_query.empty? ? "" : "?#{result_query}")
|
141
|
-
end
|
142
|
-
|
143
|
-
def h(str)
|
144
|
-
return CGI.escapeHTML(str.to_s())
|
145
|
-
end
|
146
|
-
|
147
|
-
end
|
148
|
-
|
149
|
-
|
150
|
-
# Raised when spreadsheets.google.com has returned error.
|
151
|
-
class Error < RuntimeError
|
152
|
-
|
153
|
-
end
|
154
|
-
|
155
|
-
|
156
|
-
# Raised when GoogleSpreadsheet.login has failed.
|
157
|
-
class AuthenticationError < GoogleSpreadsheet::Error
|
158
|
-
|
159
|
-
end
|
160
|
-
|
161
|
-
|
162
|
-
class ClientLoginFetcher #:nodoc:
|
163
|
-
|
164
|
-
def initialize(auth_tokens, proxy)
|
165
|
-
@auth_tokens = auth_tokens
|
166
|
-
if proxy
|
167
|
-
@proxy = proxy
|
168
|
-
elsif ENV["http_proxy"] && !ENV["http_proxy"].empty?
|
169
|
-
proxy_url = URI.parse(ENV["http_proxy"])
|
170
|
-
@proxy = Net::HTTP.Proxy(proxy_url.host, proxy_url.port)
|
171
|
-
else
|
172
|
-
@proxy = Net::HTTP
|
173
|
-
end
|
174
|
-
end
|
175
|
-
|
176
|
-
attr_accessor(:auth_tokens)
|
177
|
-
|
178
|
-
def request_raw(method, url, data, extra_header, auth)
|
179
|
-
uri = URI.parse(url)
|
180
|
-
http = @proxy.new(uri.host, uri.port)
|
181
|
-
http.use_ssl = true
|
182
|
-
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
183
|
-
http.start() do
|
184
|
-
path = uri.path + (uri.query ? "?#{uri.query}" : "")
|
185
|
-
header = auth_header(auth).merge(extra_header)
|
186
|
-
if method == :delete || method == :get
|
187
|
-
return http.__send__(method, path, header)
|
188
|
-
else
|
189
|
-
return http.__send__(method, path, data, header)
|
190
|
-
end
|
191
|
-
end
|
192
|
-
end
|
193
|
-
|
194
|
-
private
|
195
|
-
|
196
|
-
def auth_header(auth)
|
197
|
-
token = auth == :none ? nil : @auth_tokens[auth]
|
198
|
-
if token
|
199
|
-
return {"Authorization" => "GoogleLogin auth=#{token}"}
|
200
|
-
else
|
201
|
-
return {}
|
202
|
-
end
|
203
|
-
end
|
204
|
-
|
205
|
-
end
|
206
|
-
|
207
|
-
|
208
|
-
class OAuth1Fetcher #:nodoc:
|
209
|
-
|
210
|
-
def initialize(oauth1_token)
|
211
|
-
@oauth1_token = oauth1_token
|
212
|
-
end
|
213
|
-
|
214
|
-
def request_raw(method, url, data, extra_header, auth)
|
215
|
-
if method == :delete || method == :get
|
216
|
-
return @oauth1_token.__send__(method, url, extra_header)
|
217
|
-
else
|
218
|
-
return @oauth1_token.__send__(method, url, data, extra_header)
|
219
|
-
end
|
220
|
-
end
|
221
|
-
|
222
|
-
end
|
223
|
-
|
224
|
-
|
225
|
-
class OAuth2Fetcher #:nodoc:
|
226
|
-
|
227
|
-
Response = Struct.new(:code, :body)
|
228
|
-
|
229
|
-
def initialize(oauth2_token)
|
230
|
-
@oauth2_token = oauth2_token
|
231
|
-
end
|
232
|
-
|
233
|
-
def request_raw(method, url, data, extra_header, auth)
|
234
|
-
if method == :delete || method == :get
|
235
|
-
raw_res = @oauth2_token.request(method, url, {:header => extra_header})
|
236
|
-
else
|
237
|
-
raw_res = @oauth2_token.request(method, url, {:header => extra_header, :body => data})
|
238
|
-
end
|
239
|
-
return Response.new(raw_res.status.to_s(), raw_res.body)
|
240
|
-
end
|
241
|
-
|
242
|
-
end
|
243
|
-
|
244
|
-
|
245
|
-
# Use GoogleSpreadsheet.login or GoogleSpreadsheet.saved_session to get
|
246
|
-
# GoogleSpreadsheet::Session object.
|
247
|
-
class Session
|
248
|
-
|
249
|
-
include(Util)
|
250
|
-
extend(Util)
|
251
|
-
|
252
|
-
# The same as GoogleSpreadsheet.login.
|
253
|
-
def self.login(mail, password, proxy = nil)
|
254
|
-
session = Session.new(nil, ClientLoginFetcher.new({}, proxy))
|
255
|
-
session.login(mail, password)
|
256
|
-
return session
|
257
|
-
end
|
258
|
-
|
259
|
-
# The same as GoogleSpreadsheet.login_with_oauth.
|
260
|
-
def self.login_with_oauth(oauth_token)
|
261
|
-
case oauth_token
|
262
|
-
when OAuth::AccessToken
|
263
|
-
fetcher = OAuth1Fetcher.new(oauth_token)
|
264
|
-
when OAuth2::AccessToken
|
265
|
-
fetcher = OAuth2Fetcher.new(oauth_token)
|
266
|
-
else
|
267
|
-
raise(GoogleSpreadsheet::Error,
|
268
|
-
"oauth_token is neither OAuth::Token nor OAuth2::Token: %p" % oauth_token)
|
269
|
-
end
|
270
|
-
return Session.new(nil, fetcher)
|
271
|
-
end
|
272
|
-
|
273
|
-
# The same as GoogleSpreadsheet.restore_session.
|
274
|
-
def self.restore_session(auth_tokens, proxy = nil)
|
275
|
-
return Session.new(auth_tokens, nil, proxy)
|
276
|
-
end
|
277
|
-
|
278
|
-
# DEPRECATED: Use GoogleSpreadsheet.restore_session instead.
|
279
|
-
def initialize(auth_tokens = nil, fetcher = nil, proxy = nil)
|
280
|
-
if fetcher
|
281
|
-
@fetcher = fetcher
|
282
|
-
else
|
283
|
-
@fetcher = ClientLoginFetcher.new(auth_tokens || {}, proxy)
|
284
|
-
end
|
285
|
-
end
|
286
|
-
|
287
|
-
# Authenticates with given +mail+ and +password+, and updates current session object
|
288
|
-
# if succeeds. Raises GoogleSpreadsheet::AuthenticationError if fails.
|
289
|
-
# Google Apps account is supported.
|
290
|
-
def login(mail, password)
|
291
|
-
if !@fetcher.is_a?(ClientLoginFetcher)
|
292
|
-
raise(GoogleSpreadsheet::Error,
|
293
|
-
"Cannot call login for session created by login_with_oauth.")
|
294
|
-
end
|
295
|
-
begin
|
296
|
-
@fetcher.auth_tokens = {
|
297
|
-
:wise => authenticate(mail, password, :wise),
|
298
|
-
:writely => authenticate(mail, password, :writely),
|
299
|
-
}
|
300
|
-
rescue GoogleSpreadsheet::Error => ex
|
301
|
-
return true if @on_auth_fail && @on_auth_fail.call()
|
302
|
-
raise(AuthenticationError, "authentication failed for #{mail}: #{ex.message}")
|
303
|
-
end
|
304
|
-
end
|
305
|
-
|
306
|
-
# Authentication tokens.
|
307
|
-
def auth_tokens
|
308
|
-
if !@fetcher.is_a?(ClientLoginFetcher)
|
309
|
-
raise(GoogleSpreadsheet::Error,
|
310
|
-
"Cannot call auth_tokens for session created by " +
|
311
|
-
"login_with_oauth.")
|
312
|
-
end
|
313
|
-
return @fetcher.auth_tokens
|
314
|
-
end
|
315
|
-
|
316
|
-
# Authentication token.
|
317
|
-
def auth_token(auth = :wise)
|
318
|
-
return self.auth_tokens[auth]
|
319
|
-
end
|
320
|
-
|
321
|
-
# Proc or Method called when authentication has failed.
|
322
|
-
# When this function returns +true+, it tries again.
|
323
|
-
attr_accessor :on_auth_fail
|
324
|
-
|
325
|
-
# Returns list of spreadsheets for the user as array of GoogleSpreadsheet::Spreadsheet.
|
326
|
-
# You can specify query parameters described at
|
327
|
-
# http://code.google.com/apis/spreadsheets/docs/2.0/reference.html#Parameters
|
328
|
-
#
|
329
|
-
# e.g.
|
330
|
-
# session.spreadsheets
|
331
|
-
# session.spreadsheets("title" => "hoge")
|
332
|
-
def spreadsheets(params = {})
|
333
|
-
query = encode_query(params)
|
334
|
-
doc = request(:get, "https://spreadsheets.google.com/feeds/spreadsheets/private/full?#{query}")
|
335
|
-
result = []
|
336
|
-
doc.css("feed > entry").each() do |entry|
|
337
|
-
title = entry.css("title").text
|
338
|
-
url = entry.css(
|
339
|
-
"link[@rel='http://schemas.google.com/spreadsheets/2006#worksheetsfeed']")[0]["href"]
|
340
|
-
result.push(Spreadsheet.new(self, url, title))
|
341
|
-
end
|
342
|
-
return result
|
343
|
-
end
|
344
|
-
|
345
|
-
# Returns GoogleSpreadsheet::Spreadsheet with given +key+.
|
346
|
-
#
|
347
|
-
# e.g.
|
348
|
-
# # http://spreadsheets.google.com/ccc?key=pz7XtlQC-PYx-jrVMJErTcg&hl=ja
|
349
|
-
# session.spreadsheet_by_key("pz7XtlQC-PYx-jrVMJErTcg")
|
350
|
-
def spreadsheet_by_key(key)
|
351
|
-
url = "https://spreadsheets.google.com/feeds/worksheets/#{key}/private/full"
|
352
|
-
return Spreadsheet.new(self, url)
|
353
|
-
end
|
354
|
-
|
355
|
-
# Returns GoogleSpreadsheet::Spreadsheet with given +url+. You must specify either of:
|
356
|
-
# - URL of the page you open to access the spreadsheet in your browser
|
357
|
-
# - URL of worksheet-based feed of the spreadseet
|
358
|
-
#
|
359
|
-
# e.g.
|
360
|
-
# session.spreadsheet_by_url(
|
361
|
-
# "http://spreadsheets.google.com/ccc?key=pz7XtlQC-PYx-jrVMJErTcg&hl=en")
|
362
|
-
# session.spreadsheet_by_url(
|
363
|
-
# "https://spreadsheets.google.com/feeds/worksheets/pz7XtlQC-PYx-jrVMJErTcg/private/full")
|
364
|
-
def spreadsheet_by_url(url)
|
365
|
-
# Tries to parse it as URL of human-readable spreadsheet.
|
366
|
-
uri = URI.parse(url)
|
367
|
-
if uri.host == "spreadsheets.google.com" && uri.path =~ /\/ccc$/
|
368
|
-
if (uri.query || "").split(/&/).find(){ |s| s=~ /^key=(.*)$/ }
|
369
|
-
return spreadsheet_by_key($1)
|
370
|
-
end
|
371
|
-
end
|
372
|
-
# Assumes the URL is worksheets feed URL.
|
373
|
-
return Spreadsheet.new(self, url)
|
374
|
-
end
|
375
|
-
|
376
|
-
# Returns GoogleSpreadsheet::Worksheet with given +url+.
|
377
|
-
# You must specify URL of cell-based feed of the worksheet.
|
378
|
-
#
|
379
|
-
# e.g.
|
380
|
-
# session.worksheet_by_url(
|
381
|
-
# "http://spreadsheets.google.com/feeds/cells/pz7XtlQC-PYxNmbBVgyiNWg/od6/private/full")
|
382
|
-
def worksheet_by_url(url)
|
383
|
-
return Worksheet.new(self, nil, url)
|
384
|
-
end
|
385
|
-
|
386
|
-
# Returns GoogleSpreadsheet::Collection with given +url+.
|
387
|
-
# You must specify URL of collection (folder) feed.
|
388
|
-
#
|
389
|
-
# e.g.
|
390
|
-
# session.collection_by_url(
|
391
|
-
# "http://docs.google.com/feeds/default/private/full/folder%3A" +
|
392
|
-
# "0B9GfDpQ2pBVUODNmOGE0NjIzMWU3ZC00NmUyLTk5NzEtYaFkZjY1MjAyxjMc")
|
393
|
-
def collection_by_url(url)
|
394
|
-
return Collection.new(self, url)
|
395
|
-
end
|
396
|
-
|
397
|
-
# Creates new spreadsheet and returns the new GoogleSpreadsheet::Spreadsheet.
|
398
|
-
#
|
399
|
-
# e.g.
|
400
|
-
# session.create_spreadsheet("My new sheet")
|
401
|
-
def create_spreadsheet(
|
402
|
-
title = "Untitled",
|
403
|
-
feed_url = "https://docs.google.com/feeds/documents/private/full")
|
404
|
-
xml = <<-"EOS"
|
405
|
-
<atom:entry xmlns:atom="http://www.w3.org/2005/Atom" xmlns:docs="http://schemas.google.com/docs/2007">
|
406
|
-
<atom:category scheme="http://schemas.google.com/g/2005#kind"
|
407
|
-
term="http://schemas.google.com/docs/2007#spreadsheet" label="spreadsheet"/>
|
408
|
-
<atom:title>#{h(title)}</atom:title>
|
409
|
-
</atom:entry>
|
410
|
-
EOS
|
411
|
-
|
412
|
-
doc = request(:post, feed_url, :data => xml, :auth => :writely)
|
413
|
-
ss_url = doc.css(
|
414
|
-
"link[@rel='http://schemas.google.com/spreadsheets/2006#worksheetsfeed']").first['href']
|
415
|
-
return Spreadsheet.new(self, ss_url, title)
|
416
|
-
end
|
417
|
-
|
418
|
-
def request(method, url, params = {}) #:nodoc:
|
419
|
-
# Always uses HTTPS.
|
420
|
-
url = url.gsub(%r{^http://}, "https://")
|
421
|
-
data = params[:data]
|
422
|
-
auth = params[:auth] || :wise
|
423
|
-
if params[:header]
|
424
|
-
extra_header = params[:header]
|
425
|
-
elsif data
|
426
|
-
extra_header = {"Content-Type" => "application/atom+xml"}
|
427
|
-
else
|
428
|
-
extra_header = {}
|
429
|
-
end
|
430
|
-
response_type = params[:response_type] || :xml
|
431
|
-
|
432
|
-
while true
|
433
|
-
response = @fetcher.request_raw(method, url, data, extra_header, auth)
|
434
|
-
if response.code == "401" && @on_auth_fail && @on_auth_fail.call()
|
435
|
-
next
|
436
|
-
end
|
437
|
-
if !(response.code =~ /^2/)
|
438
|
-
raise(
|
439
|
-
response.code == "401" ? AuthenticationError : GoogleSpreadsheet::Error,
|
440
|
-
"Response code #{response.code} for #{method} #{url}: " +
|
441
|
-
CGI.unescapeHTML(response.body))
|
442
|
-
end
|
443
|
-
return convert_response(response, response_type)
|
444
|
-
end
|
445
|
-
end
|
446
|
-
|
447
|
-
def inspect
|
448
|
-
return '#<%p:0x%x>' % [self.class, self.object_id]
|
449
|
-
end
|
450
|
-
|
451
|
-
private
|
452
|
-
|
453
|
-
def convert_response(response, response_type)
|
454
|
-
case response_type
|
455
|
-
when :xml
|
456
|
-
return Nokogiri.XML(response.body)
|
457
|
-
when :raw
|
458
|
-
return response.body
|
459
|
-
else
|
460
|
-
raise("unknown params[:response_type]: %s" % response_type)
|
461
|
-
end
|
462
|
-
end
|
463
|
-
|
464
|
-
def authenticate(mail, password, auth)
|
465
|
-
params = {
|
466
|
-
"accountType" => "HOSTED_OR_GOOGLE",
|
467
|
-
"Email" => mail,
|
468
|
-
"Passwd" => password,
|
469
|
-
"service" => auth.to_s(),
|
470
|
-
"source" => "Gimite-RubyGoogleSpreadsheet-1.00",
|
471
|
-
}
|
472
|
-
header = {"Content-Type" => "application/x-www-form-urlencoded"}
|
473
|
-
response = request(:post,
|
474
|
-
"https://www.google.com/accounts/ClientLogin",
|
475
|
-
:data => encode_query(params), :auth => :none, :header => header, :response_type => :raw)
|
476
|
-
return response.slice(/^Auth=(.*)$/, 1)
|
477
|
-
end
|
478
|
-
|
479
|
-
end
|
480
|
-
|
481
|
-
|
482
|
-
# Use methods in GoogleSpreadsheet::Session to get GoogleSpreadsheet::Spreadsheet object.
|
483
|
-
class Spreadsheet
|
484
|
-
|
485
|
-
include(Util)
|
486
|
-
|
487
|
-
SUPPORTED_EXPORT_FORMAT = Set.new(["xls", "csv", "pdf", "ods", "tsv", "html"])
|
488
|
-
|
489
|
-
def initialize(session, worksheets_feed_url, title = nil) #:nodoc:
|
490
|
-
@session = session
|
491
|
-
@worksheets_feed_url = worksheets_feed_url
|
492
|
-
@title = title
|
493
|
-
end
|
494
|
-
|
495
|
-
# URL of worksheet-based feed of the spreadsheet.
|
496
|
-
attr_reader(:worksheets_feed_url)
|
497
|
-
|
498
|
-
# Title of the spreadsheet.
|
499
|
-
#
|
500
|
-
# Set params[:reload] to true to force reloading the title.
|
501
|
-
def title(params = {})
|
502
|
-
if !@title || params[:reload]
|
503
|
-
@title = spreadsheet_feed_entry(params).css("title").text
|
504
|
-
end
|
505
|
-
return @title
|
506
|
-
end
|
507
|
-
|
508
|
-
# Key of the spreadsheet.
|
509
|
-
def key
|
510
|
-
if !(@worksheets_feed_url =~
|
511
|
-
%r{^https?://spreadsheets.google.com/feeds/worksheets/(.*)/private/.*$})
|
512
|
-
raise(GoogleSpreadsheet::Error,
|
513
|
-
"worksheets feed URL is in unknown format: #{@worksheets_feed_url}")
|
514
|
-
end
|
515
|
-
return $1
|
516
|
-
end
|
517
|
-
|
518
|
-
# Spreadsheet feed URL of the spreadsheet.
|
519
|
-
def spreadsheet_feed_url
|
520
|
-
return "https://spreadsheets.google.com/feeds/spreadsheets/private/full/#{self.key}"
|
521
|
-
end
|
522
|
-
|
523
|
-
# URL which you can open the spreadsheet in a Web browser with.
|
524
|
-
#
|
525
|
-
# e.g. "http://spreadsheets.google.com/ccc?key=pz7XtlQC-PYx-jrVMJErTcg"
|
526
|
-
def human_url
|
527
|
-
# Uses Document feed because Spreadsheet feed returns wrong URL for Apps account.
|
528
|
-
return self.document_feed_entry.css("link[@rel='alternate']").first["href"]
|
529
|
-
end
|
530
|
-
|
531
|
-
# DEPRECATED: Table and Record feeds are deprecated and they will not be available after
|
532
|
-
# March 2012.
|
533
|
-
#
|
534
|
-
# Tables feed URL of the spreadsheet.
|
535
|
-
def tables_feed_url
|
536
|
-
warn(
|
537
|
-
"DEPRECATED: Google Spreadsheet Table and Record feeds are deprecated and they will " +
|
538
|
-
"not be available after March 2012.")
|
539
|
-
return "https://spreadsheets.google.com/feeds/#{self.key}/tables"
|
540
|
-
end
|
541
|
-
|
542
|
-
# URL of feed used in document list feed API.
|
543
|
-
def document_feed_url
|
544
|
-
return "https://docs.google.com/feeds/documents/private/full/spreadsheet%3A#{self.key}"
|
545
|
-
end
|
546
|
-
|
547
|
-
# <entry> element of spreadsheet feed as Nokogiri::XML::Element.
|
548
|
-
#
|
549
|
-
# Set params[:reload] to true to force reloading the feed.
|
550
|
-
def spreadsheet_feed_entry(params = {})
|
551
|
-
if !@spreadsheet_feed_entry || params[:reload]
|
552
|
-
@spreadsheet_feed_entry =
|
553
|
-
@session.request(:get, self.spreadsheet_feed_url).css("entry").first
|
554
|
-
end
|
555
|
-
return @spreadsheet_feed_entry
|
556
|
-
end
|
557
|
-
|
558
|
-
# <entry> element of document list feed as Nokogiri::XML::Element.
|
559
|
-
#
|
560
|
-
# Set params[:reload] to true to force reloading the feed.
|
561
|
-
def document_feed_entry(params = {})
|
562
|
-
if !@document_feed_entry || params[:reload]
|
563
|
-
@document_feed_entry =
|
564
|
-
@session.request(:get, self.document_feed_url, :auth => :writely).css("entry").first
|
565
|
-
end
|
566
|
-
return @document_feed_entry
|
567
|
-
end
|
568
|
-
|
569
|
-
# Creates copy of this spreadsheet with the given title.
|
570
|
-
def duplicate(new_title = nil)
|
571
|
-
new_title ||= (self.title ? "Copy of " + self.title : "Untitled")
|
572
|
-
post_url = "https://docs.google.com/feeds/default/private/full/"
|
573
|
-
header = {"GData-Version" => "3.0", "Content-Type" => "application/atom+xml"}
|
574
|
-
xml = <<-"EOS"
|
575
|
-
<entry xmlns='http://www.w3.org/2005/Atom'>
|
576
|
-
<id>#{h(self.document_feed_url)}</id>
|
577
|
-
<title>#{h(new_title)}</title>
|
578
|
-
</entry>
|
579
|
-
EOS
|
580
|
-
doc = @session.request(:post, post_url, :data => xml, :header => header, :auth => :writely)
|
581
|
-
ss_url = doc.css(
|
582
|
-
"link[@rel='http://schemas.google.com/spreadsheets/2006#worksheetsfeed']").first["href"]
|
583
|
-
return Spreadsheet.new(@session, ss_url, new_title)
|
584
|
-
end
|
585
|
-
|
586
|
-
# If +permanent+ is +false+, moves the spreadsheet to the trash.
|
587
|
-
# If +permanent+ is +true+, deletes the spreadsheet permanently.
|
588
|
-
def delete(permanent = false)
|
589
|
-
@session.request(:delete,
|
590
|
-
self.document_feed_url + (permanent ? "?delete=true" : ""),
|
591
|
-
:auth => :writely, :header => {"If-Match" => "*"})
|
592
|
-
end
|
593
|
-
|
594
|
-
# Renames title of the spreadsheet.
|
595
|
-
def rename(title)
|
596
|
-
doc = @session.request(:get, self.document_feed_url, :auth => :writely)
|
597
|
-
edit_url = doc.css("link[@rel='edit']").first["href"]
|
598
|
-
xml = <<-"EOS"
|
599
|
-
<atom:entry
|
600
|
-
xmlns:atom="http://www.w3.org/2005/Atom"
|
601
|
-
xmlns:docs="http://schemas.google.com/docs/2007">
|
602
|
-
<atom:category
|
603
|
-
scheme="http://schemas.google.com/g/2005#kind"
|
604
|
-
term="http://schemas.google.com/docs/2007#spreadsheet" label="spreadsheet"/>
|
605
|
-
<atom:title>#{h(title)}</atom:title>
|
606
|
-
</atom:entry>
|
607
|
-
EOS
|
608
|
-
|
609
|
-
@session.request(:put, edit_url, :data => xml, :auth => :writely)
|
610
|
-
end
|
611
|
-
|
612
|
-
alias title= rename
|
613
|
-
|
614
|
-
# Exports the spreadsheet in +format+ and returns it as String.
|
615
|
-
#
|
616
|
-
# +format+ can be either "xls", "csv", "pdf", "ods", "tsv" or "html".
|
617
|
-
# In format such as "csv", only the worksheet specified with +worksheet_index+ is exported.
|
618
|
-
def export_as_string(format, worksheet_index = nil)
|
619
|
-
gid_param = worksheet_index ? "&gid=#{worksheet_index}" : ""
|
620
|
-
url =
|
621
|
-
"https://spreadsheets.google.com/feeds/download/spreadsheets/Export" +
|
622
|
-
"?key=#{key}&exportFormat=#{format}#{gid_param}"
|
623
|
-
return @session.request(:get, url, :response_type => :raw)
|
624
|
-
end
|
625
|
-
|
626
|
-
# Exports the spreadsheet in +format+ as a local file.
|
627
|
-
#
|
628
|
-
# +format+ can be either "xls", "csv", "pdf", "ods", "tsv" or "html".
|
629
|
-
# If +format+ is nil, it is guessed from the file name.
|
630
|
-
# In format such as "csv", only the worksheet specified with +worksheet_index+ is exported.
|
631
|
-
def export_as_file(local_path, format = nil, worksheet_index = nil)
|
632
|
-
if !format
|
633
|
-
format = File.extname(local_path).gsub(/^\./, "")
|
634
|
-
if !SUPPORTED_EXPORT_FORMAT.include?(format)
|
635
|
-
raise(ArgumentError,
|
636
|
-
("Cannot guess format from the file name: %s\n" +
|
637
|
-
"Specify format argument explicitly") %
|
638
|
-
local_path)
|
639
|
-
end
|
640
|
-
end
|
641
|
-
open(local_path, "wb") do |f|
|
642
|
-
f.write(export_as_string(format, worksheet_index))
|
643
|
-
end
|
644
|
-
end
|
645
|
-
|
646
|
-
# Returns worksheets of the spreadsheet as array of GoogleSpreadsheet::Worksheet.
|
647
|
-
def worksheets
|
648
|
-
doc = @session.request(:get, @worksheets_feed_url)
|
649
|
-
result = []
|
650
|
-
doc.css('entry').each() do |entry|
|
651
|
-
title = entry.css('title').text
|
652
|
-
url = entry.css(
|
653
|
-
"link[@rel='http://schemas.google.com/spreadsheets/2006#cellsfeed']").first['href']
|
654
|
-
result.push(Worksheet.new(@session, self, url, title))
|
655
|
-
end
|
656
|
-
return result.freeze()
|
657
|
-
end
|
658
|
-
|
659
|
-
# Returns a GoogleSpreadsheet::Worksheet with the given title in the spreadsheet.
|
660
|
-
#
|
661
|
-
# Returns nil if not found. Returns the first one when multiple worksheets with the
|
662
|
-
# title are found.
|
663
|
-
def worksheet_by_title(title)
|
664
|
-
return self.worksheets.find(){ |ws| ws.title == title }
|
665
|
-
end
|
666
|
-
|
667
|
-
# Adds a new worksheet to the spreadsheet. Returns added GoogleSpreadsheet::Worksheet.
|
668
|
-
def add_worksheet(title, max_rows = 100, max_cols = 20)
|
669
|
-
xml = <<-"EOS"
|
670
|
-
<entry xmlns='http://www.w3.org/2005/Atom'
|
671
|
-
xmlns:gs='http://schemas.google.com/spreadsheets/2006'>
|
672
|
-
<title>#{h(title)}</title>
|
673
|
-
<gs:rowCount>#{h(max_rows)}</gs:rowCount>
|
674
|
-
<gs:colCount>#{h(max_cols)}</gs:colCount>
|
675
|
-
</entry>
|
676
|
-
EOS
|
677
|
-
doc = @session.request(:post, @worksheets_feed_url, :data => xml)
|
678
|
-
url = doc.css(
|
679
|
-
"link[@rel='http://schemas.google.com/spreadsheets/2006#cellsfeed']").first['href']
|
680
|
-
return Worksheet.new(@session, self, url, title)
|
681
|
-
end
|
682
|
-
|
683
|
-
# DEPRECATED: Table and Record feeds are deprecated and they will not be available after
|
684
|
-
# March 2012.
|
685
|
-
#
|
686
|
-
# Returns list of tables in the spreadsheet.
|
687
|
-
def tables
|
688
|
-
warn(
|
689
|
-
"DEPRECATED: Google Spreadsheet Table and Record feeds are deprecated and they will " +
|
690
|
-
"not be available after March 2012.")
|
691
|
-
doc = @session.request(:get, self.tables_feed_url)
|
692
|
-
return doc.css('entry').map(){ |e| Table.new(@session, e) }.freeze()
|
693
|
-
end
|
694
|
-
|
695
|
-
def inspect
|
696
|
-
fields = {:worksheets_feed_url => self.worksheets_feed_url}
|
697
|
-
fields[:title] = @title if @title
|
698
|
-
return '#<%p %s>' % [self.class, fields.map(){ |k, v| "%s=%p" % [k, v] }.join(", ")]
|
699
|
-
end
|
700
|
-
|
701
|
-
end
|
702
|
-
|
703
|
-
# Use GoogleSpreadsheet::Session#collection_by_url to get GoogleSpreadsheet::Collection object.
|
704
|
-
class Collection
|
705
|
-
|
706
|
-
include(Util)
|
707
|
-
|
708
|
-
def initialize(session, collection_feed_url) #:nodoc:
|
709
|
-
@session = session
|
710
|
-
@collection_feed_url = collection_feed_url
|
711
|
-
end
|
712
|
-
|
713
|
-
# Adds the given GoogleSpreadsheet::Spreadsheet to the collection.
|
714
|
-
def add(spreadsheet)
|
715
|
-
contents_url = concat_url(@collection_feed_url, "/contents")
|
716
|
-
header = {"GData-Version" => "3.0", "Content-Type" => "application/atom+xml"}
|
717
|
-
xml = <<-"EOS"
|
718
|
-
<entry xmlns="http://www.w3.org/2005/Atom">
|
719
|
-
<id>#{h(spreadsheet.document_feed_url)}</id>
|
720
|
-
</entry>
|
721
|
-
EOS
|
722
|
-
@session.request(
|
723
|
-
:post, contents_url, :data => xml, :header => header, :auth => :writely)
|
724
|
-
return nil
|
725
|
-
end
|
726
|
-
|
727
|
-
# Returns all the spreadsheets in the collection.
|
728
|
-
def spreadsheets
|
729
|
-
contents_url = concat_url(@collection_feed_url, "/contents")
|
730
|
-
header = {"GData-Version" => "3.0", "Content-Type" => "application/atom+xml"}
|
731
|
-
doc = @session.request(:get, contents_url, :header => header, :auth => :writely)
|
732
|
-
|
733
|
-
return doc.css("feed > entry").map() do |entry|
|
734
|
-
title = entry.css("title").text
|
735
|
-
url = entry.css(
|
736
|
-
"link[@rel='http://schemas.google.com/spreadsheets/2006#worksheetsfeed']")[0]["href"]
|
737
|
-
GoogleSpreadsheet::Spreadsheet.new(@session, url, title)
|
738
|
-
end
|
739
|
-
end
|
740
|
-
|
741
|
-
# TODO Add other operations.
|
742
|
-
|
743
|
-
end
|
744
|
-
|
745
|
-
# DEPRECATED: Table and Record feeds are deprecated and they will not be available after
|
746
|
-
# March 2012.
|
747
|
-
#
|
748
|
-
# Use GoogleSpreadsheet::Worksheet#add_table to create table.
|
749
|
-
# Use GoogleSpreadsheet::Worksheet#tables to get GoogleSpreadsheet::Table objects.
|
750
|
-
class Table
|
751
|
-
|
752
|
-
include(Util)
|
753
|
-
|
754
|
-
def initialize(session, entry) #:nodoc:
|
755
|
-
@columns = {}
|
756
|
-
@worksheet_title = entry.css('gs|worksheet').first['name']
|
757
|
-
@records_url = entry.css("content")[0]["src"]
|
758
|
-
@edit_url = entry.css("link[@rel='edit']")[0]['href']
|
759
|
-
@session = session
|
760
|
-
end
|
761
|
-
|
762
|
-
# Title of the worksheet the table belongs to.
|
763
|
-
attr_reader(:worksheet_title)
|
764
|
-
|
765
|
-
# Adds a record.
|
766
|
-
def add_record(values)
|
767
|
-
fields = ""
|
768
|
-
values.each() do |name, value|
|
769
|
-
fields += "<gs:field name='#{h(name)}'>#{h(value)}</gs:field>"
|
770
|
-
end
|
771
|
-
xml =<<-EOS
|
772
|
-
<entry
|
773
|
-
xmlns="http://www.w3.org/2005/Atom"
|
774
|
-
xmlns:gs="http://schemas.google.com/spreadsheets/2006">
|
775
|
-
#{fields}
|
776
|
-
</entry>
|
777
|
-
EOS
|
778
|
-
@session.request(:post, @records_url, :data => xml)
|
779
|
-
end
|
780
|
-
|
781
|
-
# Returns records in the table.
|
782
|
-
def records
|
783
|
-
doc = @session.request(:get, @records_url)
|
784
|
-
return doc.css('entry').map(){ |e| Record.new(@session, e) }
|
785
|
-
end
|
786
|
-
|
787
|
-
# Deletes this table. Deletion takes effect right away without calling save().
|
788
|
-
def delete
|
789
|
-
@session.request(:delete, @edit_url, :header => {"If-Match" => "*"})
|
790
|
-
end
|
791
|
-
|
792
|
-
end
|
793
|
-
|
794
|
-
# DEPRECATED: Table and Record feeds are deprecated and they will not be available after
|
795
|
-
# March 2012.
|
796
|
-
#
|
797
|
-
# Use GoogleSpreadsheet::Table#records to get GoogleSpreadsheet::Record objects.
|
798
|
-
class Record < Hash
|
799
|
-
include(Util)
|
800
|
-
|
801
|
-
def initialize(session, entry) #:nodoc:
|
802
|
-
@session = session
|
803
|
-
entry.css('gs|field').each() do |field|
|
804
|
-
self[field["name"]] = field.inner_text
|
805
|
-
end
|
806
|
-
end
|
807
|
-
|
808
|
-
def inspect #:nodoc:
|
809
|
-
content = self.map(){ |k, v| "%p => %p" % [k, v] }.join(", ")
|
810
|
-
return "\#<%p:{%s}>" % [self.class, content]
|
811
|
-
end
|
812
|
-
|
813
|
-
end
|
814
|
-
|
815
|
-
# A worksheet (i.e. a tab) in a spreadsheet.
|
816
|
-
# Use GoogleSpreadsheet::Spreadsheet#worksheets to get GoogleSpreadsheet::Worksheet object.
|
817
|
-
class Worksheet
|
818
|
-
|
819
|
-
include(Util)
|
820
|
-
|
821
|
-
def initialize(session, spreadsheet, cells_feed_url, title = nil) #:nodoc:
|
822
|
-
@session = session
|
823
|
-
@spreadsheet = spreadsheet
|
824
|
-
@cells_feed_url = cells_feed_url
|
825
|
-
@title = title
|
826
|
-
|
827
|
-
@cells = nil
|
828
|
-
@input_values = nil
|
829
|
-
@modified = Set.new()
|
830
|
-
@list = nil
|
831
|
-
end
|
832
|
-
|
833
|
-
# URL of cell-based feed of the worksheet.
|
834
|
-
attr_reader(:cells_feed_url)
|
835
|
-
|
836
|
-
# URL of worksheet feed URL of the worksheet.
|
837
|
-
def worksheet_feed_url
|
838
|
-
# I don't know good way to get worksheet feed URL from cells feed URL.
|
839
|
-
# Probably it would be cleaner to keep worksheet feed URL and get cells feed URL
|
840
|
-
# from it.
|
841
|
-
if !(@cells_feed_url =~
|
842
|
-
%r{^https?://spreadsheets.google.com/feeds/cells/(.*)/(.*)/private/full((\?.*)?)$})
|
843
|
-
raise(GoogleSpreadsheet::Error,
|
844
|
-
"cells feed URL is in unknown format: #{@cells_feed_url}")
|
845
|
-
end
|
846
|
-
return "https://spreadsheets.google.com/feeds/worksheets/#{$1}/private/full/#{$2}#{$3}"
|
847
|
-
end
|
848
|
-
|
849
|
-
# GoogleSpreadsheet::Spreadsheet which this worksheet belongs to.
|
850
|
-
def spreadsheet
|
851
|
-
if !@spreadsheet
|
852
|
-
if !(@cells_feed_url =~
|
853
|
-
%r{^https?://spreadsheets.google.com/feeds/cells/(.*)/(.*)/private/full(\?.*)?$})
|
854
|
-
raise(GoogleSpreadsheet::Error,
|
855
|
-
"cells feed URL is in unknown format: #{@cells_feed_url}")
|
856
|
-
end
|
857
|
-
@spreadsheet = @session.spreadsheet_by_key($1)
|
858
|
-
end
|
859
|
-
return @spreadsheet
|
860
|
-
end
|
861
|
-
|
862
|
-
# Returns content of the cell as String. Top-left cell is [1, 1].
|
863
|
-
def [](row, col)
|
864
|
-
return self.cells[[row, col]] || ""
|
865
|
-
end
|
866
|
-
|
867
|
-
# Updates content of the cell.
|
868
|
-
# Note that update is not sent to the server until you call save().
|
869
|
-
# Top-left cell is [1, 1].
|
870
|
-
#
|
871
|
-
# e.g.
|
872
|
-
# worksheet[2, 1] = "hoge"
|
873
|
-
# worksheet[1, 3] = "=A1+B1"
|
874
|
-
def []=(row, col, value)
|
875
|
-
reload() if !@cells
|
876
|
-
@cells[[row, col]] = value
|
877
|
-
@input_values[[row, col]] = value
|
878
|
-
@modified.add([row, col])
|
879
|
-
self.max_rows = row if row > @max_rows
|
880
|
-
self.max_cols = col if col > @max_cols
|
881
|
-
end
|
882
|
-
|
883
|
-
# Returns the value or the formula of the cell. Top-left cell is [1, 1].
|
884
|
-
#
|
885
|
-
# If user input "=A1+B1" to cell [1, 3]:
|
886
|
-
# worksheet[1, 3] #=> "3" for example
|
887
|
-
# worksheet.input_value(1, 3) #=> "=RC[-2]+RC[-1]"
|
888
|
-
def input_value(row, col)
|
889
|
-
reload() if !@cells
|
890
|
-
return @input_values[[row, col]] || ""
|
891
|
-
end
|
892
|
-
|
893
|
-
# Row number of the bottom-most non-empty row.
|
894
|
-
def num_rows
|
895
|
-
reload() if !@cells
|
896
|
-
return @input_values.select(){ |(r, c), v| !v.empty? }.map(){ |(r, c), v| r }.max || 0
|
897
|
-
end
|
898
|
-
|
899
|
-
# Column number of the right-most non-empty column.
|
900
|
-
def num_cols
|
901
|
-
reload() if !@cells
|
902
|
-
return @input_values.select(){ |(r, c), v| !v.empty? }.map(){ |(r, c), v| c }.max || 0
|
903
|
-
end
|
904
|
-
|
905
|
-
# Number of rows including empty rows.
|
906
|
-
def max_rows
|
907
|
-
reload() if !@cells
|
908
|
-
return @max_rows
|
909
|
-
end
|
910
|
-
|
911
|
-
# Updates number of rows.
|
912
|
-
# Note that update is not sent to the server until you call save().
|
913
|
-
def max_rows=(rows)
|
914
|
-
reload() if !@cells
|
915
|
-
@max_rows = rows
|
916
|
-
@meta_modified = true
|
917
|
-
end
|
918
|
-
|
919
|
-
# Number of columns including empty columns.
|
920
|
-
def max_cols
|
921
|
-
reload() if !@cells
|
922
|
-
return @max_cols
|
923
|
-
end
|
924
|
-
|
925
|
-
# Updates number of columns.
|
926
|
-
# Note that update is not sent to the server until you call save().
|
927
|
-
def max_cols=(cols)
|
928
|
-
reload() if !@cells
|
929
|
-
@max_cols = cols
|
930
|
-
@meta_modified = true
|
931
|
-
end
|
932
|
-
|
933
|
-
# Title of the worksheet (shown as tab label in Web interface).
|
934
|
-
def title
|
935
|
-
reload() if !@title
|
936
|
-
return @title
|
937
|
-
end
|
938
|
-
|
939
|
-
# Updates title of the worksheet.
|
940
|
-
# Note that update is not sent to the server until you call save().
|
941
|
-
def title=(title)
|
942
|
-
reload() if !@cells
|
943
|
-
@title = title
|
944
|
-
@meta_modified = true
|
945
|
-
end
|
946
|
-
|
947
|
-
def cells #:nodoc:
|
948
|
-
reload() if !@cells
|
949
|
-
return @cells
|
950
|
-
end
|
951
|
-
|
952
|
-
# An array of spreadsheet rows. Each row contains an array of
|
953
|
-
# columns. Note that resulting array is 0-origin so
|
954
|
-
# worksheet.rows[0][0] == worksheet[1, 1].
|
955
|
-
def rows(skip = 0)
|
956
|
-
nc = self.num_cols
|
957
|
-
result = ((1 + skip)..self.num_rows).map() do |row|
|
958
|
-
(1..nc).map(){ |col| self[row, col] }.freeze()
|
959
|
-
end
|
960
|
-
return result.freeze()
|
961
|
-
end
|
962
|
-
|
963
|
-
# Reloads content of the worksheets from the server.
|
964
|
-
# Note that changes you made by []= etc. is discarded if you haven't called save().
|
965
|
-
def reload()
|
966
|
-
doc = @session.request(:get, @cells_feed_url)
|
967
|
-
@max_rows = doc.css('gs|rowCount').text.to_i
|
968
|
-
@max_cols = doc.css('gs|colCount').text.to_i
|
969
|
-
@title = doc.css('feed > title')[0].text
|
970
|
-
|
971
|
-
@cells = {}
|
972
|
-
@input_values = {}
|
973
|
-
doc.css('feed > entry').each() do |entry|
|
974
|
-
cell = entry.css('gs|cell').first
|
975
|
-
row = cell["row"].to_i()
|
976
|
-
col = cell["col"].to_i()
|
977
|
-
@cells[[row, col]] = cell.inner_text
|
978
|
-
@input_values[[row, col]] = cell["inputValue"]
|
979
|
-
end
|
980
|
-
@modified.clear()
|
981
|
-
@meta_modified = false
|
982
|
-
return true
|
983
|
-
end
|
984
|
-
|
985
|
-
# Saves your changes made by []=, etc. to the server.
|
986
|
-
def save()
|
987
|
-
sent = false
|
988
|
-
|
989
|
-
if @meta_modified
|
990
|
-
|
991
|
-
ws_doc = @session.request(:get, self.worksheet_feed_url)
|
992
|
-
edit_url = ws_doc.css("link[@rel='edit']").first['href']
|
993
|
-
xml = <<-"EOS"
|
994
|
-
<entry xmlns='http://www.w3.org/2005/Atom'
|
995
|
-
xmlns:gs='http://schemas.google.com/spreadsheets/2006'>
|
996
|
-
<title>#{h(self.title)}</title>
|
997
|
-
<gs:rowCount>#{h(self.max_rows)}</gs:rowCount>
|
998
|
-
<gs:colCount>#{h(self.max_cols)}</gs:colCount>
|
999
|
-
</entry>
|
1000
|
-
EOS
|
1001
|
-
|
1002
|
-
@session.request(:put, edit_url, :data => xml)
|
1003
|
-
|
1004
|
-
@meta_modified = false
|
1005
|
-
sent = true
|
1006
|
-
|
1007
|
-
end
|
1008
|
-
|
1009
|
-
if !@modified.empty?
|
1010
|
-
|
1011
|
-
# Gets id and edit URL for each cell.
|
1012
|
-
# Note that return-empty=true is required to get those info for empty cells.
|
1013
|
-
cell_entries = {}
|
1014
|
-
rows = @modified.map(){ |r, c| r }
|
1015
|
-
cols = @modified.map(){ |r, c| c }
|
1016
|
-
url = concat_url(@cells_feed_url,
|
1017
|
-
"?return-empty=true&min-row=#{rows.min}&max-row=#{rows.max}" +
|
1018
|
-
"&min-col=#{cols.min}&max-col=#{cols.max}")
|
1019
|
-
doc = @session.request(:get, url)
|
1020
|
-
|
1021
|
-
doc.css('entry').each() do |entry|
|
1022
|
-
row = entry.css('gs|cell').first['row'].to_i
|
1023
|
-
col = entry.css('gs|cell').first['col'].to_i
|
1024
|
-
cell_entries[[row, col]] = entry
|
1025
|
-
end
|
1026
|
-
|
1027
|
-
# Updates cell values using batch operation.
|
1028
|
-
# If the data is large, we split it into multiple operations, otherwise batch may fail.
|
1029
|
-
@modified.each_slice(250) do |chunk|
|
1030
|
-
|
1031
|
-
xml = <<-EOS
|
1032
|
-
<feed xmlns="http://www.w3.org/2005/Atom"
|
1033
|
-
xmlns:batch="http://schemas.google.com/gdata/batch"
|
1034
|
-
xmlns:gs="http://schemas.google.com/spreadsheets/2006">
|
1035
|
-
<id>#{h(@cells_feed_url)}</id>
|
1036
|
-
EOS
|
1037
|
-
for row, col in chunk
|
1038
|
-
value = @cells[[row, col]]
|
1039
|
-
entry = cell_entries[[row, col]]
|
1040
|
-
id = entry.css('id').text
|
1041
|
-
edit_url = entry.css("link[@rel='edit']").first['href']
|
1042
|
-
xml << <<-EOS
|
1043
|
-
<entry>
|
1044
|
-
<batch:id>#{h(row)},#{h(col)}</batch:id>
|
1045
|
-
<batch:operation type="update"/>
|
1046
|
-
<id>#{h(id)}</id>
|
1047
|
-
<link rel="edit" type="application/atom+xml"
|
1048
|
-
href="#{h(edit_url)}"/>
|
1049
|
-
<gs:cell row="#{h(row)}" col="#{h(col)}" inputValue="#{h(value)}"/>
|
1050
|
-
</entry>
|
1051
|
-
EOS
|
1052
|
-
end
|
1053
|
-
xml << <<-"EOS"
|
1054
|
-
</feed>
|
1055
|
-
EOS
|
1056
|
-
|
1057
|
-
batch_url = concat_url(@cells_feed_url, "/batch")
|
1058
|
-
result = @session.request(:post, batch_url, :data => xml)
|
1059
|
-
result.css('atom|entry').each() do |entry|
|
1060
|
-
interrupted = entry.css('batch|interrupted').first
|
1061
|
-
if interrupted
|
1062
|
-
raise(GoogleSpreadsheet::Error, "Update has failed: %s" %
|
1063
|
-
interrupted["reason"])
|
1064
|
-
end
|
1065
|
-
if !(entry.css('batch|status').first['code'] =~ /^2/)
|
1066
|
-
raise(GoogleSpreadsheet::Error, "Updating cell %s has failed: %s" %
|
1067
|
-
[entry.css('atom|id').text, entry.css('batch|status').first['reason']])
|
1068
|
-
end
|
1069
|
-
end
|
1070
|
-
|
1071
|
-
end
|
1072
|
-
|
1073
|
-
@modified.clear()
|
1074
|
-
sent = true
|
1075
|
-
|
1076
|
-
end
|
1077
|
-
return sent
|
1078
|
-
end
|
1079
|
-
|
1080
|
-
# Calls save() and reload().
|
1081
|
-
def synchronize()
|
1082
|
-
save()
|
1083
|
-
reload()
|
1084
|
-
end
|
1085
|
-
|
1086
|
-
# Deletes this worksheet. Deletion takes effect right away without calling save().
|
1087
|
-
def delete()
|
1088
|
-
ws_doc = @session.request(:get, self.worksheet_feed_url)
|
1089
|
-
edit_url = ws_doc.css("link[@rel='edit']").first['href']
|
1090
|
-
@session.request(:delete, edit_url)
|
1091
|
-
end
|
1092
|
-
|
1093
|
-
# Returns true if you have changes made by []= which haven't been saved.
|
1094
|
-
def dirty?
|
1095
|
-
return !@modified.empty?
|
1096
|
-
end
|
1097
|
-
|
1098
|
-
# DEPRECATED: Table and Record feeds are deprecated and they will not be available after
|
1099
|
-
# March 2012.
|
1100
|
-
#
|
1101
|
-
# Creates table for the worksheet and returns GoogleSpreadsheet::Table.
|
1102
|
-
# See this document for details:
|
1103
|
-
# http://code.google.com/intl/en/apis/spreadsheets/docs/3.0/developers_guide_protocol.html#TableFeeds
|
1104
|
-
def add_table(table_title, summary, columns, options)
|
1105
|
-
warn(
|
1106
|
-
"DEPRECATED: Google Spreadsheet Table and Record feeds are deprecated and they will " +
|
1107
|
-
"not be available after March 2012.")
|
1108
|
-
default_options = { :header_row => 1, :num_rows => 0, :start_row => 2}
|
1109
|
-
options = default_options.merge(options)
|
1110
|
-
|
1111
|
-
column_xml = ""
|
1112
|
-
columns.each() do |index, name|
|
1113
|
-
column_xml += "<gs:column index='#{h(index)}' name='#{h(name)}'/>\n"
|
1114
|
-
end
|
1115
|
-
|
1116
|
-
xml = <<-"EOS"
|
1117
|
-
<entry xmlns="http://www.w3.org/2005/Atom"
|
1118
|
-
xmlns:gs="http://schemas.google.com/spreadsheets/2006">
|
1119
|
-
<title type='text'>#{h(table_title)}</title>
|
1120
|
-
<summary type='text'>#{h(summary)}</summary>
|
1121
|
-
<gs:worksheet name='#{h(self.title)}' />
|
1122
|
-
<gs:header row='#{options[:header_row]}' />
|
1123
|
-
<gs:data numRows='#{options[:num_rows]}' startRow='#{options[:start_row]}'>
|
1124
|
-
#{column_xml}
|
1125
|
-
</gs:data>
|
1126
|
-
</entry>
|
1127
|
-
EOS
|
1128
|
-
|
1129
|
-
result = @session.request(:post, self.spreadsheet.tables_feed_url, :data => xml)
|
1130
|
-
return Table.new(@session, result)
|
1131
|
-
end
|
1132
|
-
|
1133
|
-
# Returns list of tables for the workwheet.
|
1134
|
-
def tables
|
1135
|
-
return self.spreadsheet.tables.select(){ |t| t.worksheet_title == self.title }
|
1136
|
-
end
|
1137
|
-
|
1138
|
-
# List feed URL of the worksheet.
|
1139
|
-
def list_feed_url
|
1140
|
-
# Gets the worksheets metafeed.
|
1141
|
-
entry = @session.request(:get, self.worksheet_feed_url)
|
1142
|
-
|
1143
|
-
# Gets the URL of list-based feed for the given spreadsheet.
|
1144
|
-
return entry.css(
|
1145
|
-
"link[@rel='http://schemas.google.com/spreadsheets/2006#listfeed']").first['href']
|
1146
|
-
end
|
1147
|
-
|
1148
|
-
# Provides access to cells using column names, assuming the first row contains column
|
1149
|
-
# names. Returned object is GoogleSpreadsheet::List which you can use mostly as
|
1150
|
-
# Array of Hash.
|
1151
|
-
#
|
1152
|
-
# e.g. Assuming the first row is ["x", "y"]:
|
1153
|
-
# worksheet.list[0]["x"] #=> "1" # i.e. worksheet[2, 1]
|
1154
|
-
# worksheet.list[0]["y"] #=> "2" # i.e. worksheet[2, 2]
|
1155
|
-
# worksheet.list[1]["x"] = "3" # i.e. worksheet[3, 1] = "3"
|
1156
|
-
# worksheet.list[1]["y"] = "4" # i.e. worksheet[3, 2] = "4"
|
1157
|
-
# worksheet.list.push({"x" => "5", "y" => "6"})
|
1158
|
-
#
|
1159
|
-
# Note that update is not sent to the server until you call save().
|
1160
|
-
def list
|
1161
|
-
return @list ||= List.new(self)
|
1162
|
-
end
|
1163
|
-
|
1164
|
-
def inspect
|
1165
|
-
fields = {:worksheet_feed_url => self.worksheet_feed_url}
|
1166
|
-
fields[:title] = @title if @title
|
1167
|
-
return '#<%p %s>' % [self.class, fields.map(){ |k, v| "%s=%p" % [k, v] }.join(", ")]
|
1168
|
-
end
|
1169
|
-
|
1170
|
-
end
|
1171
|
-
|
1172
|
-
|
1173
|
-
# Provides access to cells using column names.
|
1174
|
-
# Use GoogleSpreadsheet::Worksheet#list to get GoogleSpreadsheet::List object.
|
1175
|
-
#--
|
1176
|
-
# This is implemented as wrapper of GoogleSpreadsheet::Worksheet i.e. using cells
|
1177
|
-
# feed, not list feed. In this way, we can easily provide consistent API as
|
1178
|
-
# GoogleSpreadsheet::Worksheet using save()/reload().
|
1179
|
-
class List
|
1180
|
-
|
1181
|
-
include(Enumerable)
|
1182
|
-
|
1183
|
-
def initialize(worksheet) #:nodoc:
|
1184
|
-
@worksheet = worksheet
|
1185
|
-
end
|
1186
|
-
|
1187
|
-
# Number of non-empty rows in the worksheet excluding the first row.
|
1188
|
-
def size
|
1189
|
-
return @worksheet.num_rows - 1
|
1190
|
-
end
|
1191
|
-
|
1192
|
-
# Returns Hash-like object (GoogleSpreadsheet::ListRow) for the row with the
|
1193
|
-
# index. Keys of the object are colum names (the first row).
|
1194
|
-
# The second row has index 0.
|
1195
|
-
#
|
1196
|
-
# Note that updates to the returned object are not sent to the server until
|
1197
|
-
# you call GoogleSpreadsheet::Worksheet#save().
|
1198
|
-
def [](index)
|
1199
|
-
return ListRow.new(self, index)
|
1200
|
-
end
|
1201
|
-
|
1202
|
-
# Updates the row with the index with the given Hash object.
|
1203
|
-
# Keys of +hash+ are colum names (the first row).
|
1204
|
-
# The second row has index 0.
|
1205
|
-
#
|
1206
|
-
# Note that update is not sent to the server until
|
1207
|
-
# you call GoogleSpreadsheet::Worksheet#save().
|
1208
|
-
def []=(index, hash)
|
1209
|
-
self[index].replace(hash)
|
1210
|
-
end
|
1211
|
-
|
1212
|
-
# Iterates over Hash-like object (GoogleSpreadsheet::ListRow) for each row
|
1213
|
-
# (except for the first row).
|
1214
|
-
# Keys of the object are colum names (the first row).
|
1215
|
-
def each(&block)
|
1216
|
-
for i in 0...self.size
|
1217
|
-
yield(self[i])
|
1218
|
-
end
|
1219
|
-
end
|
1220
|
-
|
1221
|
-
# Column names i.e. the contents of the first row.
|
1222
|
-
# Duplicates are removed.
|
1223
|
-
def keys
|
1224
|
-
return (1..@worksheet.num_cols).map(){ |i| @worksheet[1, i] }.uniq()
|
1225
|
-
end
|
1226
|
-
|
1227
|
-
# Updates column names i.e. the contents of the first row.
|
1228
|
-
#
|
1229
|
-
# Note that update is not sent to the server until
|
1230
|
-
# you call GoogleSpreadsheet::Worksheet#save().
|
1231
|
-
def keys=(ary)
|
1232
|
-
for i in 1..ary.size
|
1233
|
-
@worksheet[1, i] = ary[i - 1]
|
1234
|
-
end
|
1235
|
-
for i in (ary.size + 1)..@worksheet.num_cols
|
1236
|
-
@worksheet[1, i] = ""
|
1237
|
-
end
|
1238
|
-
end
|
1239
|
-
|
1240
|
-
# Adds a new row to the bottom.
|
1241
|
-
# Keys of +hash+ are colum names (the first row).
|
1242
|
-
# Returns GoogleSpreadsheet::ListRow for the new row.
|
1243
|
-
#
|
1244
|
-
# Note that update is not sent to the server until
|
1245
|
-
# you call GoogleSpreadsheet::Worksheet#save().
|
1246
|
-
def push(hash)
|
1247
|
-
row = self[self.size]
|
1248
|
-
row.update(hash)
|
1249
|
-
return row
|
1250
|
-
end
|
1251
|
-
|
1252
|
-
# Returns all rows (except for the first row) as Array of Hash.
|
1253
|
-
# Keys of Hash objects are colum names (the first row).
|
1254
|
-
def to_hash_array()
|
1255
|
-
return self.map(){ |r| r.to_hash() }
|
1256
|
-
end
|
1257
|
-
|
1258
|
-
def get(index, key) #:nodoc:
|
1259
|
-
return @worksheet[index + 2, key_to_col(key)]
|
1260
|
-
end
|
1261
|
-
|
1262
|
-
def set(index, key, value) #:nodoc:
|
1263
|
-
@worksheet[index + 2, key_to_col(key)] = value
|
1264
|
-
end
|
1265
|
-
|
1266
|
-
private
|
1267
|
-
|
1268
|
-
def key_to_col(key)
|
1269
|
-
key = key.to_s()
|
1270
|
-
col = (1..@worksheet.num_cols).find(){ |c| @worksheet[1, c] == key }
|
1271
|
-
raise(GoogleSpreadsheet::Error, "colunm doesn't exist: %p" % key) if !col
|
1272
|
-
return col
|
1273
|
-
end
|
1274
|
-
|
1275
|
-
end
|
1276
|
-
|
1277
|
-
# Hash-like object returned by GoogleSpreadsheet::List#[].
|
1278
|
-
class ListRow
|
1279
|
-
|
1280
|
-
include(Enumerable)
|
1281
|
-
extend(Forwardable)
|
1282
|
-
|
1283
|
-
def_delegators(:to_hash,
|
1284
|
-
:keys, :values, :each_key, :each_value, :each, :each_pair, :hash,
|
1285
|
-
:assoc, :fetch, :flatten, :key, :invert, :size, :length, :rassoc,
|
1286
|
-
:merge, :reject, :select, :sort, :to_a, :values_at)
|
1287
|
-
|
1288
|
-
def initialize(list, index) #:nodoc:
|
1289
|
-
@list = list
|
1290
|
-
@index = index
|
1291
|
-
end
|
1292
|
-
|
1293
|
-
def [](key)
|
1294
|
-
return @list.get(@index, key)
|
1295
|
-
end
|
1296
|
-
|
1297
|
-
def []=(key, value)
|
1298
|
-
@list.set(@index, key, value)
|
1299
|
-
end
|
1300
|
-
|
1301
|
-
def has_key?(key)
|
1302
|
-
return @list.keys.include?(key)
|
1303
|
-
end
|
1304
|
-
|
1305
|
-
alias include? has_key?
|
1306
|
-
alias key? has_key?
|
1307
|
-
alias member? has_key?
|
1308
|
-
|
1309
|
-
def update(hash)
|
1310
|
-
for k, v in hash
|
1311
|
-
self[k] = v
|
1312
|
-
end
|
1313
|
-
end
|
1314
|
-
|
1315
|
-
alias merge! update
|
1316
|
-
|
1317
|
-
def replace(hash)
|
1318
|
-
clear()
|
1319
|
-
update(hash)
|
1320
|
-
end
|
1321
|
-
|
1322
|
-
def clear()
|
1323
|
-
for key in @list.keys
|
1324
|
-
self[key] = ""
|
1325
|
-
end
|
1326
|
-
end
|
1327
|
-
|
1328
|
-
def to_hash()
|
1329
|
-
result = {}
|
1330
|
-
for key in @list.keys
|
1331
|
-
result[key] = self[key]
|
1332
|
-
end
|
1333
|
-
return result
|
1334
|
-
end
|
1335
|
-
|
1336
|
-
def ==(other)
|
1337
|
-
return self.class == other.class && self.to_hash() == other.to_hash()
|
1338
|
-
end
|
1339
|
-
|
1340
|
-
alias === ==
|
1341
|
-
alias eql? ==
|
1342
|
-
|
1343
|
-
def inspect
|
1344
|
-
return "\#<%p %p>" % [self.class, to_hash()]
|
1345
|
-
end
|
1346
|
-
|
1347
|
-
end
|
1348
119
|
|
1349
120
|
end
|