google-spreadsheet-ruby 0.0.8

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.
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
+