commondream-google-spreadsheet-ruby 0.0.4
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.txt +56 -0
- data/lib/google_spreadsheet.rb +558 -0
- metadata +65 -0
data/README.txt
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
This is a 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
|
+
# Logs in.
|
18
|
+
session = GoogleSpreadsheet.login("username@gmail.com", "mypassword")
|
19
|
+
|
20
|
+
# First worksheet of http://spreadsheets.google.com/ccc?key=pz7XtlQC-PYx-jrVMJErTcg&hl=en
|
21
|
+
ws = session.spreadsheet_by_key("pz7XtlQC-PYx-jrVMJErTcg").worksheets[0]
|
22
|
+
|
23
|
+
# Gets content of A2 cell.
|
24
|
+
p ws[2, 1] #==> "hoge"
|
25
|
+
|
26
|
+
# Changes content of cells. Changes are not sent to the server until you call ws.save().
|
27
|
+
ws[2, 1] = "foo"
|
28
|
+
ws[2, 2] = "bar"
|
29
|
+
ws.save()
|
30
|
+
|
31
|
+
# Dumps all cells.
|
32
|
+
for row in 1..ws.num_rows
|
33
|
+
for col in 1..ws.num_cols
|
34
|
+
p ws[row, col]
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# Yet another way to do so.
|
39
|
+
p ws.rows #==> [["fuga", ""], ["foo", "bar]]
|
40
|
+
|
41
|
+
# Reloads the worksheet to get changes by other clients.
|
42
|
+
ws.reload()
|
43
|
+
|
44
|
+
API document: http://gimite.net/gimite/rubymess/google-spreadsheet-ruby/
|
45
|
+
|
46
|
+
|
47
|
+
= Source code
|
48
|
+
|
49
|
+
http://github.com/gimite/google-spreadsheet-ruby/tree/master
|
50
|
+
|
51
|
+
The license of this source is "New BSD Licence"
|
52
|
+
|
53
|
+
|
54
|
+
= Author
|
55
|
+
|
56
|
+
Hiroshi Ichikawa - http://gimite.net/en/index.php?Contact
|
@@ -0,0 +1,558 @@
|
|
1
|
+
# Author: Hiroshi Ichikawa <http://gimite.net/>
|
2
|
+
# The license of this source is "New BSD Licence"
|
3
|
+
|
4
|
+
require "set"
|
5
|
+
require "net/https"
|
6
|
+
require "open-uri"
|
7
|
+
require "cgi"
|
8
|
+
require "rubygems"
|
9
|
+
require "hpricot"
|
10
|
+
Net::HTTP.version_1_2
|
11
|
+
|
12
|
+
|
13
|
+
module GoogleSpreadsheet
|
14
|
+
|
15
|
+
# Authenticates with given +mail+ and +password+, and returns GoogleSpreadsheet::Session
|
16
|
+
# if succeeds. Raises GoogleSpreadsheet::AuthenticationError if fails.
|
17
|
+
# Google Apps account is supported.
|
18
|
+
def self.login(mail, password)
|
19
|
+
return Session.login(mail, password)
|
20
|
+
end
|
21
|
+
|
22
|
+
# Restores GoogleSpreadsheet::Session from +path+ and returns it.
|
23
|
+
# If +path+ doesn't exist or authentication has failed, prompts mail and password on console,
|
24
|
+
# authenticates with them, stores the session to +path+ and returns it.
|
25
|
+
#
|
26
|
+
# This method requires Ruby/Password library: http://www.caliban.org/ruby/ruby-password.shtml
|
27
|
+
def self.saved_session(path = ENV["HOME"] + "/.ruby_google_spreadsheet.token")
|
28
|
+
session = Session.new(File.exist?(path) ? File.read(path) : nil)
|
29
|
+
session.on_auth_fail = proc() do
|
30
|
+
require "password"
|
31
|
+
$stderr.print("Mail: ")
|
32
|
+
mail = $stdin.gets().chomp()
|
33
|
+
password = Password.get()
|
34
|
+
session.login(mail, password)
|
35
|
+
open(path, "w", 0600){ |f| f.write(session.auth_token) }
|
36
|
+
true
|
37
|
+
end
|
38
|
+
if !session.auth_token
|
39
|
+
session.on_auth_fail.call()
|
40
|
+
end
|
41
|
+
return session
|
42
|
+
end
|
43
|
+
|
44
|
+
|
45
|
+
module Util #:nodoc:
|
46
|
+
|
47
|
+
module_function
|
48
|
+
|
49
|
+
def http_request(method, url, data, header = {})
|
50
|
+
uri = URI.parse(url)
|
51
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
52
|
+
http.use_ssl = uri.scheme == "https"
|
53
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
54
|
+
http.start() do
|
55
|
+
path = uri.path + (uri.query ? "?#{uri.query}" : "")
|
56
|
+
if method == :delete
|
57
|
+
response = http.__send__(method, path, header)
|
58
|
+
else
|
59
|
+
response = http.__send__(method, path, data, header)
|
60
|
+
end
|
61
|
+
|
62
|
+
if !(response.code =~ /^2/)
|
63
|
+
raise(GoogleSpreadsheet::Error, "Response code #{response.code} for POST #{url}: " +
|
64
|
+
CGI.unescapeHTML(response.body))
|
65
|
+
end
|
66
|
+
return response.body
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def encode_query(params)
|
71
|
+
return params.map(){ |k, v| uri_encode(k) + "=" + uri_encode(v) }.join("&")
|
72
|
+
end
|
73
|
+
|
74
|
+
def uri_encode(str)
|
75
|
+
return URI.encode(str, /#{URI::UNSAFE}|&/n)
|
76
|
+
end
|
77
|
+
|
78
|
+
def h(str)
|
79
|
+
return CGI.escapeHTML(str.to_s())
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
83
|
+
|
84
|
+
|
85
|
+
# Raised when spreadsheets.google.com has returned error.
|
86
|
+
class Error < RuntimeError
|
87
|
+
|
88
|
+
end
|
89
|
+
|
90
|
+
|
91
|
+
# Raised when GoogleSpreadsheet.login has failed.
|
92
|
+
class AuthenticationError < GoogleSpreadsheet::Error
|
93
|
+
|
94
|
+
end
|
95
|
+
|
96
|
+
|
97
|
+
# Use GoogleSpreadsheet.login or GoogleSpreadsheet.saved_session to get
|
98
|
+
# GoogleSpreadsheet::Session object.
|
99
|
+
class Session
|
100
|
+
|
101
|
+
include(Util)
|
102
|
+
extend(Util)
|
103
|
+
|
104
|
+
# The same as GoogleSpreadsheet.login.
|
105
|
+
def self.login(mail, password)
|
106
|
+
session = Session.new()
|
107
|
+
session.login(mail, password)
|
108
|
+
return session
|
109
|
+
end
|
110
|
+
|
111
|
+
# Creates session object with given authentication token.
|
112
|
+
def initialize(auth_token = nil)
|
113
|
+
@auth_token = auth_token
|
114
|
+
end
|
115
|
+
|
116
|
+
# Authenticates with given +mail+ and +password+, and updates current session object
|
117
|
+
# if succeeds. Raises GoogleSpreadsheet::AuthenticationError if fails.
|
118
|
+
# Google Apps account is supported.
|
119
|
+
def login(mail, password)
|
120
|
+
begin
|
121
|
+
@auth_token = nil
|
122
|
+
params = {
|
123
|
+
"accountType" => "HOSTED_OR_GOOGLE",
|
124
|
+
"Email" => mail,
|
125
|
+
"Passwd" => password,
|
126
|
+
"service" => "wise",
|
127
|
+
"source" => "Gimite-RubyGoogleSpreadsheet-1.00",
|
128
|
+
}
|
129
|
+
response = http_request(:post,
|
130
|
+
"https://www.google.com/accounts/ClientLogin", encode_query(params))
|
131
|
+
@auth_token = response.slice(/^Auth=(.*)$/, 1)
|
132
|
+
rescue GoogleSpreadsheet::Error => ex
|
133
|
+
return true if @on_auth_fail && @on_auth_fail.call()
|
134
|
+
raise(AuthenticationError, "authentication failed for #{mail}: #{ex.message}")
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
# Authentication token.
|
139
|
+
attr_accessor(:auth_token)
|
140
|
+
|
141
|
+
# Proc or Method called when authentication has failed.
|
142
|
+
# When this function returns +true+, it tries again.
|
143
|
+
attr_accessor(:on_auth_fail)
|
144
|
+
|
145
|
+
def get(url) #:nodoc:
|
146
|
+
while true
|
147
|
+
begin
|
148
|
+
response = open(url, self.http_header){ |f| f.read() }
|
149
|
+
rescue OpenURI::HTTPError => ex
|
150
|
+
if ex.message =~ /^401/ && @on_auth_fail && @on_auth_fail.call()
|
151
|
+
next
|
152
|
+
end
|
153
|
+
raise(ex.message =~ /^401/ ? AuthenticationError : GoogleSpreadsheet::Error,
|
154
|
+
"Error #{ex.message} for GET #{url}: " + ex.io.read())
|
155
|
+
end
|
156
|
+
return Hpricot.XML(response)
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
def post(url, data) #:nodoc:
|
161
|
+
header = self.http_header.merge({"Content-Type" => "application/atom+xml"})
|
162
|
+
response = http_request(:post, url, data, header)
|
163
|
+
return Hpricot.XML(response)
|
164
|
+
end
|
165
|
+
|
166
|
+
def put(url, data) #:nodoc:
|
167
|
+
header = self.http_header.merge({"Content-Type" => "application/atom+xml"})
|
168
|
+
response = http_request(:put, url, data, header)
|
169
|
+
return Hpricot.XML(response)
|
170
|
+
end
|
171
|
+
|
172
|
+
def delete(url)
|
173
|
+
header = self.http_header.merge({"Content-Type" => "application/atom+xml"})
|
174
|
+
response = http_request(:delete, url, nil, header)
|
175
|
+
return Hpricot.XML(response)
|
176
|
+
end
|
177
|
+
|
178
|
+
def http_header #:nodoc:
|
179
|
+
return {"Authorization" => "GoogleLogin auth=#{@auth_token}"}
|
180
|
+
end
|
181
|
+
|
182
|
+
# Returns list of spreadsheets for the user as array of GoogleSpreadsheet::Spreadsheet.
|
183
|
+
# You can specify query parameters described at
|
184
|
+
# http://code.google.com/apis/spreadsheets/docs/2.0/reference.html#Parameters
|
185
|
+
#
|
186
|
+
# e.g.
|
187
|
+
# session.spreadsheets
|
188
|
+
# session.spreadsheets("title" => "hoge")
|
189
|
+
def spreadsheets(params = {})
|
190
|
+
query = encode_query(params)
|
191
|
+
doc = get("http://spreadsheets.google.com/feeds/spreadsheets/private/full?#{query}")
|
192
|
+
result = []
|
193
|
+
for entry in doc.search("entry")
|
194
|
+
title = entry.search("title").text
|
195
|
+
url = entry.search(
|
196
|
+
"link[@rel='http://schemas.google.com/spreadsheets/2006#worksheetsfeed']")[0]["href"]
|
197
|
+
result.push(Spreadsheet.new(self, url, title))
|
198
|
+
end
|
199
|
+
return result
|
200
|
+
end
|
201
|
+
|
202
|
+
# Returns GoogleSpreadsheet::Spreadsheet with given +key+.
|
203
|
+
#
|
204
|
+
# e.g.
|
205
|
+
# # http://spreadsheets.google.com/ccc?key=pz7XtlQC-PYx-jrVMJErTcg&hl=ja
|
206
|
+
# session.spreadsheet_by_key("pz7XtlQC-PYx-jrVMJErTcg")
|
207
|
+
def spreadsheet_by_key(key)
|
208
|
+
url = "http://spreadsheets.google.com/feeds/worksheets/#{key}/private/full"
|
209
|
+
return Spreadsheet.new(self, url)
|
210
|
+
end
|
211
|
+
|
212
|
+
# Returns GoogleSpreadsheet::Spreadsheet with given +url+. You must specify either of:
|
213
|
+
# - URL of the page you open to access the spreadsheet in your browser
|
214
|
+
# - URL of worksheet-based feed of the spreadseet
|
215
|
+
#
|
216
|
+
# e.g.
|
217
|
+
# session.spreadsheet_by_url(
|
218
|
+
# "http://spreadsheets.google.com/ccc?key=pz7XtlQC-PYx-jrVMJErTcg&hl=en")
|
219
|
+
# session.spreadsheet_by_url(
|
220
|
+
# "http://spreadsheets.google.com/feeds/worksheets/pz7XtlQC-PYx-jrVMJErTcg/private/full")
|
221
|
+
def spreadsheet_by_url(url)
|
222
|
+
# Tries to parse it as URL of human-readable spreadsheet.
|
223
|
+
uri = URI.parse(url)
|
224
|
+
if uri.host == "spreadsheets.google.com" && uri.path =~ /\/ccc$/
|
225
|
+
if (uri.query || "").split(/&/).find(){ |s| s=~ /^key=(.*)$/ }
|
226
|
+
return spreadsheet_by_key($1)
|
227
|
+
end
|
228
|
+
end
|
229
|
+
# Assumes the URL is worksheets feed URL.
|
230
|
+
return Spreadsheet.new(self, url)
|
231
|
+
end
|
232
|
+
|
233
|
+
# Returns GoogleSpreadsheet::Worksheet with given +url+.
|
234
|
+
# You must specify URL of cell-based feed of the worksheet.
|
235
|
+
#
|
236
|
+
# e.g.
|
237
|
+
# session.worksheet_by_url(
|
238
|
+
# "http://spreadsheets.google.com/feeds/cells/pz7XtlQC-PYxNmbBVgyiNWg/od6/private/full")
|
239
|
+
def worksheet_by_url(url)
|
240
|
+
return Worksheet.new(self, url)
|
241
|
+
end
|
242
|
+
|
243
|
+
end
|
244
|
+
|
245
|
+
|
246
|
+
# Use methods in GoogleSpreadsheet::Session to get GoogleSpreadsheet::Spreadsheet object.
|
247
|
+
class Spreadsheet
|
248
|
+
|
249
|
+
include(Util)
|
250
|
+
|
251
|
+
def initialize(session, worksheets_feed_url, title = nil) #:nodoc:
|
252
|
+
@session = session
|
253
|
+
@worksheets_feed_url = worksheets_feed_url
|
254
|
+
@title = title
|
255
|
+
end
|
256
|
+
|
257
|
+
# URL of worksheet-based feed of the spreadsheet.
|
258
|
+
attr_reader(:worksheets_feed_url)
|
259
|
+
|
260
|
+
# Title of the spreadsheet. So far only available if you get this object by
|
261
|
+
# GoogleSpreadsheet::Session#spreadsheets.
|
262
|
+
attr_reader(:title)
|
263
|
+
|
264
|
+
# Returns worksheets of the spreadsheet as array of GoogleSpreadsheet::Worksheet.
|
265
|
+
def worksheets
|
266
|
+
doc = @session.get(@worksheets_feed_url)
|
267
|
+
result = []
|
268
|
+
for entry in doc.search("entry")
|
269
|
+
title = entry.search("title").text
|
270
|
+
|
271
|
+
url = entry.search(
|
272
|
+
"link[@rel='http://schemas.google.com/spreadsheets/2006#cellsfeed']")[0]["href"]
|
273
|
+
result.push(Worksheet.new(@session, url, title))
|
274
|
+
end
|
275
|
+
return result.freeze()
|
276
|
+
end
|
277
|
+
|
278
|
+
# Adds a new worksheet to the spreadsheet. Returns added GoogleSpreadsheet::Worksheet.
|
279
|
+
def add_worksheet(title, max_rows = 100, max_cols = 20)
|
280
|
+
xml = <<-"EOS"
|
281
|
+
<entry xmlns='http://www.w3.org/2005/Atom'
|
282
|
+
xmlns:gs='http://schemas.google.com/spreadsheets/2006'>
|
283
|
+
<title>#{h(title)}</title>
|
284
|
+
<gs:rowCount>#{h(max_rows)}</gs:rowCount>
|
285
|
+
<gs:colCount>#{h(max_cols)}</gs:colCount>
|
286
|
+
</entry>
|
287
|
+
EOS
|
288
|
+
doc = @session.post(@worksheets_feed_url, xml)
|
289
|
+
url = doc.search(
|
290
|
+
"link[@rel='http://schemas.google.com/spreadsheets/2006#cellsfeed']")[0]["href"]
|
291
|
+
return Worksheet.new(@session, url, title)
|
292
|
+
end
|
293
|
+
|
294
|
+
end
|
295
|
+
|
296
|
+
|
297
|
+
# Use GoogleSpreadsheet::Spreadsheet#worksheets to get GoogleSpreadsheet::Worksheet object.
|
298
|
+
class Worksheet
|
299
|
+
|
300
|
+
include(Util)
|
301
|
+
|
302
|
+
def initialize(session, cells_feed_url, title = nil) #:nodoc:
|
303
|
+
@session = session
|
304
|
+
@cells_feed_url = cells_feed_url
|
305
|
+
@title = title
|
306
|
+
|
307
|
+
@cells = nil
|
308
|
+
@input_values = nil
|
309
|
+
@modified = Set.new()
|
310
|
+
end
|
311
|
+
|
312
|
+
# URL of cell-based feed of the spreadsheet.
|
313
|
+
attr_reader(:cells_feed_url)
|
314
|
+
|
315
|
+
# Returns content of the cell as String. Top-left cell is [1, 1].
|
316
|
+
def [](row, col)
|
317
|
+
return self.cells[[row, col]] || ""
|
318
|
+
end
|
319
|
+
|
320
|
+
# Updates content of the cell.
|
321
|
+
# Note that update is not sent to the server until you call save().
|
322
|
+
# Top-left cell is [1, 1].
|
323
|
+
#
|
324
|
+
# e.g.
|
325
|
+
# worksheet[2, 1] = "hoge"
|
326
|
+
# worksheet[1, 3] = "=A1+B1"
|
327
|
+
def []=(row, col, value)
|
328
|
+
reload() if !@cells
|
329
|
+
@cells[[row, col]] = value
|
330
|
+
@input_values[[row, col]] = value
|
331
|
+
@modified.add([row, col])
|
332
|
+
self.max_rows = row if row > @max_rows
|
333
|
+
self.max_cols = col if col > @max_cols
|
334
|
+
end
|
335
|
+
|
336
|
+
# Returns the value or the formula of the cell. Top-left cell is [1, 1].
|
337
|
+
#
|
338
|
+
# If user input "=A1+B1" to cell [1, 3], worksheet[1, 3] is "3" for example and
|
339
|
+
# worksheet.input_value(1, 3) is "=RC[-2]+RC[-1]".
|
340
|
+
def input_value(row, col)
|
341
|
+
reload() if !@cells
|
342
|
+
return @input_values[[row, col]] || ""
|
343
|
+
end
|
344
|
+
|
345
|
+
# Row number of the bottom-most non-empty row.
|
346
|
+
def num_rows
|
347
|
+
reload() if !@cells
|
348
|
+
return @cells.keys.map(){ |r, c| r }.max || 0
|
349
|
+
end
|
350
|
+
|
351
|
+
# Column number of the right-most non-empty column.
|
352
|
+
def num_cols
|
353
|
+
reload() if !@cells
|
354
|
+
return @cells.keys.map(){ |r, c| c }.max || 0
|
355
|
+
end
|
356
|
+
|
357
|
+
# Number of rows including empty rows.
|
358
|
+
def max_rows
|
359
|
+
reload() if !@cells
|
360
|
+
return @max_rows
|
361
|
+
end
|
362
|
+
|
363
|
+
# Updates number of rows.
|
364
|
+
# Note that update is not sent to the server until you call save().
|
365
|
+
def max_rows=(rows)
|
366
|
+
@max_rows = rows
|
367
|
+
@meta_modified = true
|
368
|
+
end
|
369
|
+
|
370
|
+
# Number of columns including empty columns.
|
371
|
+
def max_cols
|
372
|
+
reload() if !@cells
|
373
|
+
return @max_cols
|
374
|
+
end
|
375
|
+
|
376
|
+
# Updates number of columns.
|
377
|
+
# Note that update is not sent to the server until you call save().
|
378
|
+
def max_cols=(cols)
|
379
|
+
@max_cols = cols
|
380
|
+
@meta_modified = true
|
381
|
+
end
|
382
|
+
|
383
|
+
# Title of the worksheet (shown as tab label in Web interface).
|
384
|
+
def title
|
385
|
+
reload() if !@title
|
386
|
+
return @title
|
387
|
+
end
|
388
|
+
|
389
|
+
# Updates title of the worksheet.
|
390
|
+
# Note that update is not sent to the server until you call save().
|
391
|
+
def title=(title)
|
392
|
+
@title = title
|
393
|
+
@meta_modified = true
|
394
|
+
end
|
395
|
+
|
396
|
+
def cells #:nodoc:
|
397
|
+
reload() if !@cells
|
398
|
+
return @cells
|
399
|
+
end
|
400
|
+
|
401
|
+
# An array of spreadsheet rows. Each row contains an array of
|
402
|
+
# columns. Note that resulting array is 0-origin so
|
403
|
+
# worksheet.rows[0][0] == worksheet[1, 1].
|
404
|
+
def rows(skip = 0)
|
405
|
+
nc = self.num_cols
|
406
|
+
result = ((1 + skip)..self.num_rows).map() do |row|
|
407
|
+
(1..nc).map(){ |col| self[row, col] }.freeze()
|
408
|
+
end
|
409
|
+
return result.freeze()
|
410
|
+
end
|
411
|
+
|
412
|
+
# Reloads content of the worksheets from the server.
|
413
|
+
# Note that changes you made by []= is discarded if you haven't called save().
|
414
|
+
def reload()
|
415
|
+
doc = @session.get(@cells_feed_url)
|
416
|
+
@max_rows = doc.search("gs:rowCount").text.to_i()
|
417
|
+
@max_cols = doc.search("gs:colCount").text.to_i()
|
418
|
+
@title = doc.search("title").text
|
419
|
+
|
420
|
+
@cells = {}
|
421
|
+
@input_values = {}
|
422
|
+
for entry in doc.search("entry")
|
423
|
+
cell = entry.search("gs:cell")[0]
|
424
|
+
row = cell["row"].to_i()
|
425
|
+
col = cell["col"].to_i()
|
426
|
+
@cells[[row, col]] = cell.inner_text
|
427
|
+
@input_values[[row, col]] = cell["inputValue"]
|
428
|
+
end
|
429
|
+
@modified.clear()
|
430
|
+
@meta_modified = false
|
431
|
+
return true
|
432
|
+
end
|
433
|
+
|
434
|
+
# Saves your changes made by []= to the server.
|
435
|
+
def save()
|
436
|
+
sent = false
|
437
|
+
|
438
|
+
if @meta_modified
|
439
|
+
|
440
|
+
# I don't know good way to get worksheet feed URL from cells feed URL.
|
441
|
+
# Probably it would be cleaner to keep worksheet feed URL and get cells feed URL
|
442
|
+
# from it.
|
443
|
+
if !(@cells_feed_url =~
|
444
|
+
%r{^http://spreadsheets.google.com/feeds/cells/(.*)/(.*)/private/full$})
|
445
|
+
raise(GoogleSpreadsheet::Error,
|
446
|
+
"cells feed URL is in unknown format: #{@cells_feed_url}")
|
447
|
+
end
|
448
|
+
ws_doc = @session.get(
|
449
|
+
"http://spreadsheets.google.com/feeds/worksheets/#{$1}/private/full/#{$2}")
|
450
|
+
edit_url = ws_doc.search("link[@rel='edit']")[0]["href"]
|
451
|
+
xml = <<-"EOS"
|
452
|
+
<entry xmlns='http://www.w3.org/2005/Atom'
|
453
|
+
xmlns:gs='http://schemas.google.com/spreadsheets/2006'>
|
454
|
+
<title>#{h(@title)}</title>
|
455
|
+
<gs:rowCount>#{h(@max_rows)}</gs:rowCount>
|
456
|
+
<gs:colCount>#{h(@max_cols)}</gs:colCount>
|
457
|
+
</entry>
|
458
|
+
EOS
|
459
|
+
|
460
|
+
@session.put(edit_url, xml)
|
461
|
+
|
462
|
+
@meta_modified = false
|
463
|
+
sent = true
|
464
|
+
|
465
|
+
end
|
466
|
+
|
467
|
+
if !@modified.empty?
|
468
|
+
|
469
|
+
# Gets id and edit URL for each cell.
|
470
|
+
# Note that return-empty=true is required to get those info for empty cells.
|
471
|
+
cell_entries = {}
|
472
|
+
rows = @modified.map(){ |r, c| r }
|
473
|
+
cols = @modified.map(){ |r, c| c }
|
474
|
+
url = "#{@cells_feed_url}?return-empty=true&min-row=#{rows.min}&max-row=#{rows.max}" +
|
475
|
+
"&min-col=#{cols.min}&max-col=#{cols.max}"
|
476
|
+
doc = @session.get(url)
|
477
|
+
for entry in doc.search("entry")
|
478
|
+
row = entry.search("gs:cell")[0]["row"].to_i()
|
479
|
+
col = entry.search("gs:cell")[0]["col"].to_i()
|
480
|
+
cell_entries[[row, col]] = entry
|
481
|
+
end
|
482
|
+
|
483
|
+
# Updates cell values using batch operation.
|
484
|
+
xml = <<-"EOS"
|
485
|
+
<feed xmlns="http://www.w3.org/2005/Atom"
|
486
|
+
xmlns:batch="http://schemas.google.com/gdata/batch"
|
487
|
+
xmlns:gs="http://schemas.google.com/spreadsheets/2006">
|
488
|
+
<id>#{h(@cells_feed_url)}</id>
|
489
|
+
EOS
|
490
|
+
for row, col in @modified
|
491
|
+
value = @cells[[row, col]]
|
492
|
+
entry = cell_entries[[row, col]]
|
493
|
+
id = entry.search("id").text
|
494
|
+
edit_url = entry.search("link[@rel='edit']")[0]["href"]
|
495
|
+
xml << <<-"EOS"
|
496
|
+
<entry>
|
497
|
+
<batch:id>#{h(row)},#{h(col)}</batch:id>
|
498
|
+
<batch:operation type="update"/>
|
499
|
+
<id>#{h(id)}</id>
|
500
|
+
<link rel="edit" type="application/atom+xml"
|
501
|
+
href="#{h(edit_url)}"/>
|
502
|
+
<gs:cell row="#{h(row)}" col="#{h(col)}" inputValue="#{h(value)}"/>
|
503
|
+
</entry>
|
504
|
+
EOS
|
505
|
+
end
|
506
|
+
xml << <<-"EOS"
|
507
|
+
</feed>
|
508
|
+
EOS
|
509
|
+
|
510
|
+
result = @session.post("#{@cells_feed_url}/batch", xml)
|
511
|
+
for entry in result.search("atom:entry")
|
512
|
+
interrupted = entry.search("batch:interrupted")[0]
|
513
|
+
if interrupted
|
514
|
+
raise(GoogleSpreadsheet::Error, "Update has failed: %s" %
|
515
|
+
interrupted["reason"])
|
516
|
+
end
|
517
|
+
if !(entry.search("batch:status")[0]["code"] =~ /^2/)
|
518
|
+
raise(GoogleSpreadsheet::Error, "Updating cell %s has failed: %s" %
|
519
|
+
[entry.search("atom:id").text, entry.search("batch:status")[0]["reason"]])
|
520
|
+
end
|
521
|
+
end
|
522
|
+
|
523
|
+
@modified.clear()
|
524
|
+
sent = true
|
525
|
+
|
526
|
+
end
|
527
|
+
return sent
|
528
|
+
end
|
529
|
+
|
530
|
+
# Calls save() and reload().
|
531
|
+
def synchronize()
|
532
|
+
save()
|
533
|
+
reload()
|
534
|
+
end
|
535
|
+
|
536
|
+
# Deletes this worksheet
|
537
|
+
def delete
|
538
|
+
if !(@cells_feed_url =~
|
539
|
+
%r{^http://spreadsheets.google.com/feeds/cells/(.*)/(.*)/private/full$})
|
540
|
+
raise(GoogleSpreadsheet::Error,
|
541
|
+
"cells feed URL is in unknown format: #{@cells_feed_url}")
|
542
|
+
end
|
543
|
+
ws_doc = @session.get(
|
544
|
+
"http://spreadsheets.google.com/feeds/worksheets/#{$1}/private/full/#{$2}")
|
545
|
+
edit_url = ws_doc.search("link[@rel='edit']")[0]["href"]
|
546
|
+
|
547
|
+
@session.delete(edit_url)
|
548
|
+
end
|
549
|
+
|
550
|
+
# Returns true if you have changes made by []= which haven't been saved.
|
551
|
+
def dirty?
|
552
|
+
return !@modified.empty?
|
553
|
+
end
|
554
|
+
|
555
|
+
end
|
556
|
+
|
557
|
+
|
558
|
+
end
|
metadata
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: commondream-google-spreadsheet-ruby
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.4
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Hiroshi Ichikawa
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-07-13 00:00:00 -07:00
|
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
|
+
description: This is a library to read/write Google Spreadsheet.
|
26
|
+
email:
|
27
|
+
- gimite+github@gmail.com
|
28
|
+
executables: []
|
29
|
+
|
30
|
+
extensions: []
|
31
|
+
|
32
|
+
extra_rdoc_files:
|
33
|
+
- README.txt
|
34
|
+
files:
|
35
|
+
- README.txt
|
36
|
+
- lib/google_spreadsheet.rb
|
37
|
+
has_rdoc: true
|
38
|
+
homepage: http://github.com/gimite/google-spreadsheet-ruby/tree/master
|
39
|
+
post_install_message:
|
40
|
+
rdoc_options:
|
41
|
+
- --main
|
42
|
+
- README.txt
|
43
|
+
require_paths:
|
44
|
+
- lib
|
45
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
46
|
+
requirements:
|
47
|
+
- - ">="
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
version: "0"
|
50
|
+
version:
|
51
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - ">="
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: "0"
|
56
|
+
version:
|
57
|
+
requirements: []
|
58
|
+
|
59
|
+
rubyforge_project:
|
60
|
+
rubygems_version: 1.2.0
|
61
|
+
signing_key:
|
62
|
+
specification_version: 2
|
63
|
+
summary: This is a library to read/write Google Spreadsheet.
|
64
|
+
test_files: []
|
65
|
+
|