google-spreadsheet-ruby 0.0.8

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