gimite-google-spreadsheet-ruby 0.0.2 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (2) hide show
  1. data/lib/google_spreadsheet.rb +203 -86
  2. metadata +2 -2
@@ -20,22 +20,25 @@ module GoogleSpreadsheet
20
20
  end
21
21
 
22
22
  # Restores GoogleSpreadsheet::Session from +path+ and returns it.
23
- # If +path+ doesn't exist, prompts mail and password on console, authenticates with them,
24
- # stores the session to +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
25
  #
26
26
  # This method requires Ruby/Password library: http://www.caliban.org/ruby/ruby-password.shtml
27
27
  def self.saved_session(path = ENV["HOME"] + "/.ruby_google_spreadsheet.token")
28
- if File.exist?(path)
29
- return Session.new(File.read(path))
30
- else
28
+ session = Session.new(File.exist?(path) ? File.read(path) : nil)
29
+ session.on_auth_fail = proc() do
31
30
  require "password"
32
31
  $stderr.print("Mail: ")
33
32
  mail = $stdin.gets().chomp()
34
33
  password = Password.get()
35
- session = Session.login(mail, password)
34
+ session.login(mail, password)
36
35
  open(path, "w", 0600){ |f| f.write(session.auth_token) }
37
- return session
36
+ true
38
37
  end
38
+ if !session.auth_token
39
+ session.on_auth_fail.call()
40
+ end
41
+ return session
39
42
  end
40
43
 
41
44
 
@@ -43,14 +46,14 @@ module GoogleSpreadsheet
43
46
 
44
47
  module_function
45
48
 
46
- def http_post(url, data, header = {})
49
+ def http_request(method, url, data, header = {})
47
50
  uri = URI.parse(url)
48
51
  http = Net::HTTP.new(uri.host, uri.port)
49
52
  http.use_ssl = uri.scheme == "https"
50
53
  http.verify_mode = OpenSSL::SSL::VERIFY_NONE
51
54
  http.start() do
52
55
  path = uri.path + (uri.query ? "?#{uri.query}" : "")
53
- response = http.post(path, data, header)
56
+ response = http.__send__(method, path, data, header)
54
57
  if !(response.code =~ /^2/)
55
58
  raise(GoogleSpreadsheet::Error, "Response code #{response.code} for POST #{url}: " +
56
59
  CGI.unescapeHTML(response.body))
@@ -95,7 +98,22 @@ module GoogleSpreadsheet
95
98
 
96
99
  # The same as GoogleSpreadsheet.login.
97
100
  def self.login(mail, password)
101
+ session = Session.new()
102
+ session.login(mail, password)
103
+ return session
104
+ end
105
+
106
+ # Creates session object with given authentication token.
107
+ def initialize(auth_token = nil)
108
+ @auth_token = auth_token
109
+ end
110
+
111
+ # Authenticates with given +mail+ and +password+, and updates current session object
112
+ # if succeeds. Raises GoogleSpreadsheet::AuthenticationError if fails.
113
+ # Google Apps account is supported.
114
+ def login(mail, password)
98
115
  begin
116
+ @auth_token = nil
99
117
  params = {
100
118
  "accountType" => "HOSTED_OR_GOOGLE",
101
119
  "Email" => mail,
@@ -103,34 +121,46 @@ module GoogleSpreadsheet
103
121
  "service" => "wise",
104
122
  "source" => "Gimite-RubyGoogleSpreadsheet-1.00",
105
123
  }
106
- response = http_post("https://www.google.com/accounts/ClientLogin", encode_query(params))
107
- return Session.new(response.slice(/^Auth=(.*)$/, 1))
124
+ response = http_request(:post,
125
+ "https://www.google.com/accounts/ClientLogin", encode_query(params))
126
+ @auth_token = response.slice(/^Auth=(.*)$/, 1)
108
127
  rescue GoogleSpreadsheet::Error => ex
128
+ return true if @on_auth_fail && @on_auth_fail.call()
109
129
  raise(AuthenticationError, "authentication failed for #{mail}: #{ex.message}")
110
130
  end
111
131
  end
112
132
 
113
- # Creates session object with given authentication token.
114
- def initialize(auth_token)
115
- @auth_token = auth_token
116
- end
117
-
118
133
  # Authentication token.
119
- attr_reader(:auth_token)
134
+ attr_accessor(:auth_token)
135
+
136
+ # Proc or Method called when authentication has failed.
137
+ # When this function returns +true+, it tries again.
138
+ attr_accessor(:on_auth_fail)
120
139
 
121
140
  def get(url) #:nodoc:
122
- begin
123
- response = open(url, self.http_header){ |f| f.read() }
124
- rescue OpenURI::HTTPError => ex
125
- raise(GoogleSpreadsheet::Error, "Error #{ex.message} for GET #{url}: " +
126
- ex.io.read())
141
+ while true
142
+ begin
143
+ response = open(url, self.http_header){ |f| f.read() }
144
+ rescue OpenURI::HTTPError => ex
145
+ if ex.message =~ /^401/ && @on_auth_fail && @on_auth_fail.call()
146
+ next
147
+ end
148
+ raise(ex.message =~ /^401/ ? AuthenticationError : GoogleSpreadsheet::Error,
149
+ "Error #{ex.message} for GET #{url}: " + ex.io.read())
150
+ end
151
+ return Hpricot.XML(response)
127
152
  end
128
- return Hpricot.XML(response)
129
153
  end
130
154
 
131
155
  def post(url, data) #:nodoc:
132
156
  header = self.http_header.merge({"Content-Type" => "application/atom+xml"})
133
- response = http_post(url, data, header)
157
+ response = http_request(:post, url, data, header)
158
+ return Hpricot.XML(response)
159
+ end
160
+
161
+ def put(url, data) #:nodoc:
162
+ header = self.http_header.merge({"Content-Type" => "application/atom+xml"})
163
+ response = http_request(:put, url, data, header)
134
164
  return Hpricot.XML(response)
135
165
  end
136
166
 
@@ -262,16 +292,13 @@ module GoogleSpreadsheet
262
292
  @cells_feed_url = cells_feed_url
263
293
  @title = title
264
294
  @cells = nil
295
+ @input_values = nil
265
296
  @modified = Set.new()
266
297
  end
267
298
 
268
299
  # URL of cell-based feed of the spreadsheet.
269
300
  attr_reader(:cells_feed_url)
270
301
 
271
- # Title of the spreadsheet. So far not available if you get this object by
272
- # GoogleSpreadsheet::Spreadsheet#worksheet_by_url.
273
- attr_reader(:title)
274
-
275
302
  # Returns content of the cell as String. Top-left cell is [1, 1].
276
303
  def [](row, col)
277
304
  return self.cells[[row, col]] || ""
@@ -287,21 +314,72 @@ module GoogleSpreadsheet
287
314
  def []=(row, col, value)
288
315
  reload() if !@cells
289
316
  @cells[[row, col]] = value
317
+ @input_values[[row, col]] = value
290
318
  @modified.add([row, col])
319
+ self.max_rows = row if row > @max_rows
320
+ self.max_cols = col if col > @max_cols
291
321
  end
292
322
 
293
- # Number of the bottom-most non-empty row.
323
+ # Returns the value or the formula of the cell. Top-left cell is [1, 1].
324
+ #
325
+ # If user input "=A1+B1" to cell [1, 3], worksheet[1, 3] is "3" for example and
326
+ # worksheet.input_value(1, 3) is "=RC[-2]+RC[-1]".
327
+ def input_value(row, col)
328
+ reload() if !@cells
329
+ return @input_values[[row, col]] || ""
330
+ end
331
+
332
+ # Row number of the bottom-most non-empty row.
294
333
  def num_rows
295
334
  reload() if !@cells
296
335
  return @cells.keys.map(){ |r, c| r }.max || 0
297
336
  end
298
337
 
299
- # Number of the right-most non-empty column.
338
+ # Column number of the right-most non-empty column.
300
339
  def num_cols
301
340
  reload() if !@cells
302
341
  return @cells.keys.map(){ |r, c| c }.max || 0
303
342
  end
304
343
 
344
+ # Number of rows including empty rows.
345
+ def max_rows
346
+ reload() if !@cells
347
+ return @max_rows
348
+ end
349
+
350
+ # Updates number of rows.
351
+ # Note that update is not sent to the server until you call save().
352
+ def max_rows=(rows)
353
+ @max_rows = rows
354
+ @meta_modified = true
355
+ end
356
+
357
+ # Number of columns including empty columns.
358
+ def max_cols
359
+ reload() if !@cells
360
+ return @max_cols
361
+ end
362
+
363
+ # Updates number of columns.
364
+ # Note that update is not sent to the server until you call save().
365
+ def max_cols=(cols)
366
+ @max_cols = cols
367
+ @meta_modified = true
368
+ end
369
+
370
+ # Title of the worksheet (shown as tab label in Web interface).
371
+ def title
372
+ reload() if !@title
373
+ return @title
374
+ end
375
+
376
+ # Updates title of the worksheet.
377
+ # Note that update is not sent to the server until you call save().
378
+ def title=(title)
379
+ @title = title
380
+ @meta_modified = true
381
+ end
382
+
305
383
  def cells #:nodoc:
306
384
  reload() if !@cells
307
385
  return @cells
@@ -310,9 +388,9 @@ module GoogleSpreadsheet
310
388
  # An array of spreadsheet rows. Each row contains an array of
311
389
  # columns. Note that resulting array is 0-origin so
312
390
  # worksheet.rows[0][0] == worksheet[1, 1].
313
- def rows
391
+ def rows(skip = 0)
314
392
  nc = self.num_cols
315
- result = (1..self.num_rows).map() do |row|
393
+ result = ((1 + skip)..self.num_rows).map() do |row|
316
394
  (1..nc).map(){ |col| self[row, col] }.freeze()
317
395
  end
318
396
  return result.freeze()
@@ -322,76 +400,115 @@ module GoogleSpreadsheet
322
400
  # Note that changes you made by []= is discarded if you haven't called save().
323
401
  def reload()
324
402
  doc = @session.get(@cells_feed_url)
403
+ @max_rows = doc.search("gs:rowCount").text.to_i()
404
+ @max_cols = doc.search("gs:colCount").text.to_i()
405
+ @title = doc.search("title").text
325
406
  @cells = {}
407
+ @input_values = {}
326
408
  for entry in doc.search("entry")
327
- row = entry.search("gs:cell")[0]["row"].to_i()
328
- col = entry.search("gs:cell")[0]["col"].to_i()
329
- content = entry.search("content").text
330
- @cells[[row, col]] = content
409
+ cell = entry.search("gs:cell")[0]
410
+ row = cell["row"].to_i()
411
+ col = cell["col"].to_i()
412
+ @cells[[row, col]] = cell.inner_text
413
+ @input_values[[row, col]] = cell["inputValue"]
331
414
  end
332
415
  @modified.clear()
416
+ @meta_modified = false
333
417
  return true
334
418
  end
335
419
 
336
420
  # Saves your changes made by []= to the server.
337
421
  def save()
338
- return false if @modified.empty?
422
+ sent = false
339
423
 
340
- # Gets id and edit URL for each cell.
341
- # Note that return-empty=true is required to get those info for empty cells.
342
- cell_entries = {}
343
- rows = @modified.map(){ |r, c| r }
344
- cols = @modified.map(){ |r, c| c }
345
- url = "#{@cells_feed_url}?return-empty=true&min-row=#{rows.min}&max-row=#{rows.max}" +
346
- "&min-col=#{cols.min}&max-col=#{cols.max}"
347
- doc = @session.get(url)
348
- for entry in doc.search("entry")
349
- row = entry.search("gs:cell")[0]["row"].to_i()
350
- col = entry.search("gs:cell")[0]["col"].to_i()
351
- cell_entries[[row, col]] = entry
352
- end
353
-
354
- # Updates cell values using batch operation.
355
- xml = <<-"EOS"
356
- <feed xmlns="http://www.w3.org/2005/Atom"
357
- xmlns:batch="http://schemas.google.com/gdata/batch"
358
- xmlns:gs="http://schemas.google.com/spreadsheets/2006">
359
- <id>#{h(@cells_feed_url)}</id>
360
- EOS
361
- for row, col in @modified
362
- value = @cells[[row, col]]
363
- entry = cell_entries[[row, col]]
364
- id = entry.search("id").text
365
- edit_url = entry.search("link[@rel='edit']")[0]["href"]
366
- xml << <<-"EOS"
367
- <entry>
368
- <batch:id>#{h(row)},#{h(col)}</batch:id>
369
- <batch:operation type="update"/>
370
- <id>#{h(id)}</id>
371
- <link rel="edit" type="application/atom+xml"
372
- href="#{h(edit_url)}"/>
373
- <gs:cell row="#{h(row)}" col="#{h(col)}" inputValue="#{h(value)}"/>
424
+ if @meta_modified
425
+
426
+ # I don't know good way to get worksheet feed URL from cells feed URL.
427
+ # Probably it would be cleaner to keep worksheet feed URL and get cells feed URL
428
+ # from it.
429
+ if !(@cells_feed_url =~
430
+ %r{^http://spreadsheets.google.com/feeds/cells/(.*)/(.*)/private/full$})
431
+ raise(GoogleSpreadsheet::Error,
432
+ "cells feed URL is in unknown format: #{@cells_feed_url}")
433
+ end
434
+ ws_doc = @session.get(
435
+ "http://spreadsheets.google.com/feeds/worksheets/#{$1}/private/full/#{$2}")
436
+ edit_url = ws_doc.search("link[@rel='edit']")[0]["href"]
437
+ xml = <<-"EOS"
438
+ <entry xmlns='http://www.w3.org/2005/Atom'
439
+ xmlns:gs='http://schemas.google.com/spreadsheets/2006'>
440
+ <title>#{h(@title)}</title>
441
+ <gs:rowCount>#{h(@max_rows)}</gs:rowCount>
442
+ <gs:colCount>#{h(@max_cols)}</gs:colCount>
374
443
  </entry>
375
444
  EOS
445
+ @session.put(edit_url, xml)
446
+
447
+ @meta_modified = false
448
+ sent = true
449
+
376
450
  end
377
- xml << <<-"EOS"
378
- </feed>
379
- EOS
380
- result = @session.post("#{@cells_feed_url}/batch", xml)
381
- for entry in result.search("atom:entry")
382
- interrupted = entry.search("batch:interrupted")[0]
383
- if interrupted
384
- raise(GoogleSpreadsheet::Error, "Update has failed: %s" %
385
- interrupted["reason"])
451
+
452
+ if !@modified.empty?
453
+
454
+ # Gets id and edit URL for each cell.
455
+ # Note that return-empty=true is required to get those info for empty cells.
456
+ cell_entries = {}
457
+ rows = @modified.map(){ |r, c| r }
458
+ cols = @modified.map(){ |r, c| c }
459
+ url = "#{@cells_feed_url}?return-empty=true&min-row=#{rows.min}&max-row=#{rows.max}" +
460
+ "&min-col=#{cols.min}&max-col=#{cols.max}"
461
+ doc = @session.get(url)
462
+ for entry in doc.search("entry")
463
+ row = entry.search("gs:cell")[0]["row"].to_i()
464
+ col = entry.search("gs:cell")[0]["col"].to_i()
465
+ cell_entries[[row, col]] = entry
386
466
  end
387
- if !(entry.search("batch:status")[0]["code"] =~ /^2/)
388
- raise(GoogleSpreadsheet::Error, "Updating cell %s has failed: %s" %
389
- [entry.search("atom:id").text, entry.search("batch:status")[0]["reason"]])
467
+
468
+ # Updates cell values using batch operation.
469
+ xml = <<-"EOS"
470
+ <feed xmlns="http://www.w3.org/2005/Atom"
471
+ xmlns:batch="http://schemas.google.com/gdata/batch"
472
+ xmlns:gs="http://schemas.google.com/spreadsheets/2006">
473
+ <id>#{h(@cells_feed_url)}</id>
474
+ EOS
475
+ for row, col in @modified
476
+ value = @cells[[row, col]]
477
+ entry = cell_entries[[row, col]]
478
+ id = entry.search("id").text
479
+ edit_url = entry.search("link[@rel='edit']")[0]["href"]
480
+ xml << <<-"EOS"
481
+ <entry>
482
+ <batch:id>#{h(row)},#{h(col)}</batch:id>
483
+ <batch:operation type="update"/>
484
+ <id>#{h(id)}</id>
485
+ <link rel="edit" type="application/atom+xml"
486
+ href="#{h(edit_url)}"/>
487
+ <gs:cell row="#{h(row)}" col="#{h(col)}" inputValue="#{h(value)}"/>
488
+ </entry>
489
+ EOS
490
+ end
491
+ xml << <<-"EOS"
492
+ </feed>
493
+ EOS
494
+ result = @session.post("#{@cells_feed_url}/batch", xml)
495
+ for entry in result.search("atom:entry")
496
+ interrupted = entry.search("batch:interrupted")[0]
497
+ if interrupted
498
+ raise(GoogleSpreadsheet::Error, "Update has failed: %s" %
499
+ interrupted["reason"])
500
+ end
501
+ if !(entry.search("batch:status")[0]["code"] =~ /^2/)
502
+ raise(GoogleSpreadsheet::Error, "Updating cell %s has failed: %s" %
503
+ [entry.search("atom:id").text, entry.search("batch:status")[0]["reason"]])
504
+ end
390
505
  end
506
+
507
+ @modified.clear()
508
+ sent = true
509
+
391
510
  end
392
-
393
- @modified.clear()
394
- return true
511
+ return sent
395
512
  end
396
513
 
397
514
  # Calls save() and reload().
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gimite-google-spreadsheet-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Hiroshi Ichikawa
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2008-12-24 00:00:00 -08:00
12
+ date: 2009-07-13 00:00:00 -07:00
13
13
  default_executable:
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency