google_drive 2.1.12 → 3.0.0.pre1

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: a6e8bc240087cb507a378a4ff02886cad3e2b7ed
4
- data.tar.gz: 07a8f238c8cc89072248fd6db3b2ca0b7ab18d37
3
+ metadata.gz: 37e8e8318c40d62af473e05a2fbed3a4fc6c87c8
4
+ data.tar.gz: 8dce5cbb7f5bdcee6bf868099f45bf3cb508a3ce
5
5
  SHA512:
6
- metadata.gz: 55ff686961a787c15738c8177f6aada5b4df2851019116e120310e2523684fed2d389b9658ad7f7d832ce125bcaf608467e7f7244a76109c0c649491d14ac773
7
- data.tar.gz: b89df0376ae01ef915ae88193059a809eccbfa410f2a84a0e30c35415f08789f4021567d8da52a85e8f7cddd53319b084ddbc161deeb6ba19f0a0dd072e3cfec
6
+ metadata.gz: bccc7a33f9b60ec60efaaf7fe6cdacc7f5e0db6e4d5c05d083e2c1c31621871bcdac0fdf81acd1cb0c4b7ba40690145bdb63a619478c32c4638ca1c5431bd944
7
+ data.tar.gz: f827099020aef331134319588747b39e8acf7a58d4525a54038aed01b2e1f6ae2b16179a9fd757cd60d038bcffa795c667a69921548c423879431bb9f3d38fdf
data/README.md CHANGED
@@ -5,7 +5,7 @@ This is a Ruby library to read/write files/spreadsheets in Google Drive/Docs.
5
5
  NOTE: This is NOT a library to create Google Drive App.
6
6
 
7
7
 
8
- * [Migration from ver. 0.x.x / 1.x.x](#migration)
8
+ * [Migration from ver. 2.x.x or before](#migration)
9
9
  * [How to install](#install)
10
10
  * [How to use](#use)
11
11
  * [API documentation](http://www.rubydoc.info/gems/google_drive)
@@ -16,7 +16,7 @@ NOTE: This is NOT a library to create Google Drive App.
16
16
  * [Author](#author)
17
17
 
18
18
 
19
- ## <a name="migration">Migration from ver. 0.x.x / 1.x.x</a>
19
+ ## <a name="migration">Migration from ver. 2.x.x or before</a>
20
20
 
21
21
  There are some incompatible API changes. See
22
22
  [MIGRATING.md](https://github.com/gimite/google-drive-ruby/blob/master/MIGRATING.md).
@@ -21,7 +21,7 @@ module GoogleDrive
21
21
  def initialize(session, file)
22
22
  @session = session
23
23
  @file = file
24
- api_permissions = @session.drive.list_permissions(
24
+ api_permissions = @session.drive_service.list_permissions(
25
25
  @file.id, fields: '*', supports_team_drives: true
26
26
  )
27
27
  @entries =
@@ -70,7 +70,7 @@ module GoogleDrive
70
70
  def push(params_or_entry, options = {})
71
71
  entry = params_or_entry.is_a?(AclEntry) ?
72
72
  params_or_entry : AclEntry.new(params_or_entry)
73
- api_permission = @session.drive.create_permission(
73
+ api_permission = @session.drive_service.create_permission(
74
74
  @file.id,
75
75
  entry.params,
76
76
  { fields: '*', supports_team_drives: true }.merge(options)
@@ -85,7 +85,7 @@ module GoogleDrive
85
85
  # e.g.
86
86
  # spreadsheet.acl.delete(spreadsheet.acl[1])
87
87
  def delete(entry)
88
- @session.drive.delete_permission(
88
+ @session.drive_service.delete_permission(
89
89
  @file.id, entry.id, supports_team_drives: true
90
90
  )
91
91
  @entries.delete(entry)
@@ -93,7 +93,7 @@ module GoogleDrive
93
93
 
94
94
  # @api private
95
95
  def update_role(entry)
96
- api_permission = @session.drive.update_permission(
96
+ api_permission = @session.drive_service.update_permission(
97
97
  @file.id,
98
98
  entry.id,
99
99
  { role: entry.role },
@@ -4,6 +4,7 @@
4
4
  require 'net/https'
5
5
  require 'uri'
6
6
  require 'google/apis/drive_v3'
7
+ require 'google/apis/sheets_v4'
7
8
  Net::HTTP.version_1_2
8
9
 
9
10
  module GoogleDrive
@@ -19,30 +20,35 @@ module GoogleDrive
19
20
 
20
21
  def initialize(authorization, client_options, request_options)
21
22
  @drive = Google::Apis::DriveV3::DriveService.new
22
- @drive.authorization = authorization
23
-
24
- # Make the timeout virtually infinite because some of the operations
25
- # (e.g., uploading a large file) can take very long.
26
- # This value is the maximal allowed timeout in seconds on JRuby.
27
- t = (2**31 - 1) / 1000
28
- @drive.client_options.open_timeout_sec = t
29
- @drive.client_options.read_timeout_sec = t
30
- @drive.client_options.send_timeout_sec = t
31
-
32
- if client_options
33
- @drive.client_options.members.each do |name|
34
- if !client_options[name].nil?
35
- @drive.client_options[name] = client_options[name]
23
+ @sheets = Google::Apis::SheetsV4::SheetsService.new
24
+
25
+ [@drive, @sheets].each do |service|
26
+ service.authorization = authorization
27
+
28
+ # Make the timeout virtually infinite because some of the operations
29
+ # (e.g., uploading a large file) can take very long.
30
+ # This value is the maximal allowed timeout in seconds on JRuby.
31
+ t = (2**31 - 1) / 1000
32
+ service.client_options.open_timeout_sec = t
33
+ service.client_options.read_timeout_sec = t
34
+ service.client_options.send_timeout_sec = t
35
+
36
+ if client_options
37
+ service.client_options.members.each do |name|
38
+ if !client_options[name].nil?
39
+ service.client_options[name] = client_options[name]
40
+ end
36
41
  end
37
42
  end
38
- end
39
43
 
40
- if request_options
41
- @drive.request_options = @drive.request_options.merge(request_options)
44
+ if request_options
45
+ service.request_options = service.request_options.merge(request_options)
46
+ end
42
47
  end
43
48
  end
44
49
 
45
50
  attr_reader(:drive)
51
+ attr_reader(:sheets)
46
52
 
47
53
  def request_raw(method, url, data, extra_header, _auth)
48
54
  options = @drive.request_options.merge(header: extra_header)
@@ -19,7 +19,7 @@ module GoogleDrive
19
19
 
20
20
  # Adds the given GoogleDrive::File to the folder.
21
21
  def add(file)
22
- @session.drive.update_file(
22
+ @session.drive_service.update_file(
23
23
  file.id, add_parents: id, fields: '', supports_team_drives: true
24
24
  )
25
25
  nil
@@ -27,7 +27,7 @@ module GoogleDrive
27
27
 
28
28
  # Removes the given GoogleDrive::File from the folder.
29
29
  def remove(file)
30
- @session.drive.update_file(
30
+ @session.drive_service.update_file(
31
31
  file.id, remove_parents: id, fields: '', supports_team_drives: true
32
32
  )
33
33
  end
@@ -61,7 +61,7 @@ module GoogleDrive
61
61
  parents: [id]
62
62
  }.merge(file_properties)
63
63
 
64
- file = @session.drive.create_file(
64
+ file = @session.drive_service.create_file(
65
65
  file_metadata, fields: '*', supports_team_drives: true
66
66
  )
67
67
 
@@ -38,7 +38,7 @@ module GoogleDrive
38
38
 
39
39
  # Reloads file metadata such as title and acl.
40
40
  def reload_metadata
41
- @api_file = @session.drive.get_file(
41
+ @api_file = @session.drive_service.get_file(
42
42
  id, fields: '*', supports_team_drives: true
43
43
  )
44
44
  @acl = Acl.new(@session, self) if @acl
@@ -94,7 +94,7 @@ module GoogleDrive
94
94
  #
95
95
  # To export the file in other formats, use export_as_file.
96
96
  def download_to_file(path, params = {})
97
- @session.drive.get_file(
97
+ @session.drive_service.get_file(
98
98
  id,
99
99
  { download_dest: path, supports_team_drives: true }.merge(params)
100
100
  )
@@ -113,7 +113,7 @@ module GoogleDrive
113
113
  #
114
114
  # To export the file in other formats, use export_to_io.
115
115
  def download_to_io(io, params = {})
116
- @session.drive.get_file(
116
+ @session.drive_service.get_file(
117
117
  id,
118
118
  { download_dest: io, supports_team_drives: true }.merge(params)
119
119
  )
@@ -185,7 +185,7 @@ module GoogleDrive
185
185
  # Reads content from +io+ and updates the file with the content.
186
186
  def update_from_io(io, params = {})
187
187
  params = { upload_source: io, supports_team_drives: true }.merge(params)
188
- @session.drive.update_file(id, nil, params)
188
+ @session.drive_service.update_file(id, nil, params)
189
189
  nil
190
190
  end
191
191
 
@@ -193,9 +193,9 @@ module GoogleDrive
193
193
  # If +permanent+ is +true+, deletes the file permanently.
194
194
  def delete(permanent = false)
195
195
  if permanent
196
- @session.drive.delete_file(id, supports_team_drives: true)
196
+ @session.drive_service.delete_file(id, supports_team_drives: true)
197
197
  else
198
- @session.drive.update_file(
198
+ @session.drive_service.update_file(
199
199
  id, { trashed: true }, supports_team_drives: true
200
200
  )
201
201
  end
@@ -204,7 +204,7 @@ module GoogleDrive
204
204
 
205
205
  # Renames title of the file.
206
206
  def rename(title)
207
- @session.drive.update_file(
207
+ @session.drive_service.update_file(
208
208
  id, { name: title }, supports_team_drives: true
209
209
  )
210
210
  nil
@@ -214,7 +214,7 @@ module GoogleDrive
214
214
 
215
215
  # Creates copy of this file with the given title.
216
216
  def copy(title, file_properties = {})
217
- api_file = @session.drive.copy_file(
217
+ api_file = @session.drive_service.copy_file(
218
218
  id, { name: title }.merge(file_properties), fields: '*', supports_team_drives: true
219
219
  )
220
220
  @session.wrap_api_file(api_file)
@@ -259,7 +259,7 @@ module GoogleDrive
259
259
 
260
260
  def export_to_dest(dest, format)
261
261
  mime_type = EXT_TO_CONTENT_TYPE['.' + format] || format
262
- @session.drive.export_file(id, mime_type, download_dest: dest)
262
+ @session.drive_service.export_file(id, mime_type, download_dest: dest)
263
263
  nil
264
264
  end
265
265
  end
@@ -219,10 +219,17 @@ module GoogleDrive
219
219
  attr_accessor :on_auth_fail
220
220
 
221
221
  # Returns an instance of Google::Apis::DriveV3::DriveService.
222
- def drive
222
+ def drive_service
223
223
  @fetcher.drive
224
224
  end
225
225
 
226
+ alias drive drive_service
227
+
228
+ # Returns an instance of Google::Apis::SheetsV4::SheetsService.
229
+ def sheets_service
230
+ @fetcher.sheets
231
+ end
232
+
226
233
  # Returns list of files for the user as array of GoogleDrive::File or its
227
234
  # subclass. You can specify parameters documented at
228
235
  # https://developers.google.com/drive/v3/web/search-parameters
@@ -247,7 +254,7 @@ module GoogleDrive
247
254
  def files(params = {}, &block)
248
255
  params = convert_params(params)
249
256
  execute_paged!(
250
- method: drive.method(:list_files),
257
+ method: drive_service.method(:list_files),
251
258
  parameters: { fields: '*', supports_team_drives: true }.merge(params),
252
259
  items_method_name: :files,
253
260
  converter: proc { |af| wrap_api_file(af) },
@@ -280,7 +287,7 @@ module GoogleDrive
280
287
  # Returns an instance of GoogleDrive::File or its subclass
281
288
  # (GoogleDrive::Spreadsheet, GoogleDrive::Collection).
282
289
  def file_by_id(id)
283
- api_file = drive.get_file(id, fields: '*', supports_team_drives: true)
290
+ api_file = drive_service.get_file(id, fields: '*', supports_team_drives: true)
284
291
  wrap_api_file(api_file)
285
292
  end
286
293
 
@@ -379,13 +386,12 @@ module GoogleDrive
379
386
  # "1smypkyAz4STrKO4Zkos5Z4UPUJKvvgIza32LnlQ7OGw/od7/private/full")
380
387
  def worksheet_by_url(url)
381
388
  case url
382
- when %r{^https?://spreadsheets.google.com/feeds/worksheets/.*/.*/full/.*$}
383
- worksheet_feed_url = url
384
- when %r{^https?://spreadsheets.google.com/feeds/cells/(.*)/(.*)/private/full((\?.*)?)$}
385
- worksheet_feed_url =
386
- 'https://spreadsheets.google.com/feeds/worksheets/' \
387
- "#{Regexp.last_match(1)}/private/full/" \
388
- "#{Regexp.last_match(2)}#{Regexp.last_match(3)}"
389
+ when %r{^https?://spreadsheets.google.com/feeds/worksheets/(.*)/.*/full/(.*)$}
390
+ spreadsheet_id = Regexp.last_match(1)
391
+ worksheet_feed_id = Regexp.last_match(2)
392
+ when %r{^https?://spreadsheets.google.com/feeds/cells/(.*)/(.*)/private/full(\?.*)?$}
393
+ spreadsheet_id = Regexp.last_match(1)
394
+ worksheet_feed_id = Regexp.last_match(2)
389
395
  else
390
396
  raise(
391
397
  GoogleDrive::Error,
@@ -394,8 +400,15 @@ module GoogleDrive
394
400
  )
395
401
  end
396
402
 
397
- worksheet_feed_entry = request(:get, worksheet_feed_url)
398
- Worksheet.new(self, nil, worksheet_feed_entry)
403
+ spreadsheet = spreadsheet_by_key(spreadsheet_id)
404
+ worksheet = spreadsheet.worksheets.find{ |ws| ws.worksheet_feed_id == worksheet_feed_id }
405
+ unless worksheet
406
+ raise(
407
+ GoogleDrive::Error,
408
+ "Worksheet not found for the given URL: #{url}"
409
+ )
410
+ end
411
+ worksheet
399
412
  end
400
413
 
401
414
  # Returns the root folder.
@@ -496,7 +509,7 @@ module GoogleDrive
496
509
  name: title,
497
510
  }.merge(file_properties)
498
511
 
499
- file = drive.create_file(
512
+ file = drive_service.create_file(
500
513
  file_metadata, fields: '*', supports_team_drives: true
501
514
  )
502
515
 
@@ -661,7 +674,7 @@ module GoogleDrive
661
674
  end
662
675
  file_metadata[:parents] = params[:parents] if params[:parents]
663
676
 
664
- file = drive.create_file(file_metadata, api_params)
677
+ file = drive_service.create_file(file_metadata, api_params)
665
678
  wrap_api_file(file)
666
679
  end
667
680
 
@@ -16,6 +16,10 @@ module GoogleDrive
16
16
  # create_spreadsheet in GoogleDrive::Session to get GoogleDrive::Spreadsheet
17
17
  # object.
18
18
  class Spreadsheet < GoogleDrive::File
19
+
20
+ # TODO: Bump up the major version before switching the existing methods to
21
+ # v4 API because it requires to turn on a new API in the API console.
22
+
19
23
  include(Util)
20
24
 
21
25
  SUPPORTED_EXPORT_FORMAT = Set.new(%w[xlsx csv pdf])
@@ -45,16 +49,8 @@ module GoogleDrive
45
49
 
46
50
  # Returns worksheets of the spreadsheet as array of GoogleDrive::Worksheet.
47
51
  def worksheets
48
- doc = @session.request(:get, worksheets_feed_url)
49
- if doc.root.name != 'feed'
50
- raise(GoogleDrive::Error,
51
- format(
52
- "%s doesn't look like a worksheets feed URL because its root " \
53
- 'is not <feed>.',
54
- worksheets_feed_url
55
- ))
56
- end
57
- doc.css('entry').map { |e| Worksheet.new(@session, self, e) }.freeze
52
+ api_spreadsheet = @session.sheets_service.get_spreadsheet(id, fields: 'sheets.properties')
53
+ api_spreadsheet.sheets.map{ |s| Worksheet.new(@session, self, s.properties) }
58
54
  end
59
55
 
60
56
  # Returns a GoogleDrive::Worksheet with the given title in the spreadsheet.
@@ -68,24 +64,32 @@ module GoogleDrive
68
64
  # Returns a GoogleDrive::Worksheet with the given gid.
69
65
  #
70
66
  # Returns nil if not found.
71
- def worksheet_by_gid(gid)
72
- gid = gid.to_s
73
- worksheets.find { |ws| ws.gid == gid }
67
+ def worksheet_by_sheet_id(sheet_id)
68
+ sheet_id = sheet_id.to_i
69
+ worksheets.find { |ws| ws.sheet_id == sheet_id }
74
70
  end
75
71
 
72
+ alias worksheet_by_gid worksheet_by_sheet_id
73
+
76
74
  # Adds a new worksheet to the spreadsheet. Returns added
77
75
  # GoogleDrive::Worksheet.
78
- def add_worksheet(title, max_rows = 100, max_cols = 20)
79
- xml = <<-"EOS"
80
- <entry xmlns='http://www.w3.org/2005/Atom'
81
- xmlns:gs='http://schemas.google.com/spreadsheets/2006'>
82
- <title>#{h(title)}</title>
83
- <gs:rowCount>#{h(max_rows)}</gs:rowCount>
84
- <gs:colCount>#{h(max_cols)}</gs:colCount>
85
- </entry>
86
- EOS
87
- doc = @session.request(:post, worksheets_feed_url, data: xml)
88
- Worksheet.new(@session, self, doc.root)
76
+ #
77
+ # When +index+ is specified, the worksheet is inserted at the given
78
+ # +index+.
79
+ def add_worksheet(title, max_rows = 100, max_cols = 20, index: nil)
80
+ (response,) = batch_update([{
81
+ add_sheet: {
82
+ properties: {
83
+ title: title,
84
+ index: index,
85
+ grid_properties: {
86
+ row_count: max_rows,
87
+ column_count: max_cols,
88
+ },
89
+ },
90
+ },
91
+ }])
92
+ Worksheet.new(@session, self, response.add_sheet.properties)
89
93
  end
90
94
 
91
95
  # Not available for GoogleDrive::Spreadsheet. Use export_as_file instead.
@@ -114,5 +118,18 @@ module GoogleDrive
114
118
  'Use export_to_io instead.'
115
119
  )
116
120
  end
121
+
122
+ # Performs batch update of the spreadsheet.
123
+ #
124
+ # +requests+ is an Array of Google::Apis::SheetsV4::Request or its Hash
125
+ # equivalent. Returns an Array of Google::Apis::SheetsV4::Response.
126
+ def batch_update(requests)
127
+ batch_request =
128
+ Google::Apis::SheetsV4::BatchUpdateSpreadsheetRequest.new(
129
+ requests: requests)
130
+ batch_response =
131
+ @session.sheets_service.batch_update_spreadsheet(id, batch_request)
132
+ batch_response.replies
133
+ end
117
134
  end
118
135
  end
@@ -16,6 +16,37 @@ module GoogleDrive
16
16
  class Worksheet
17
17
  include(Util)
18
18
 
19
+ # A few default color instances that match the colors from the Google Sheets web UI.
20
+ #
21
+ # TODO: Add more colors from
22
+ # https://github.com/denilsonsa/gimp-palettes/blob/master/palettes/Google-Drive.gpl
23
+ module Colors
24
+ RED = Google::Apis::SheetsV4::Color.new(red: 1.0)
25
+ DARK_RED_1 = Google::Apis::SheetsV4::Color.new(red: 0.8)
26
+ RED_BERRY = Google::Apis::SheetsV4::Color.new(red: 0.596)
27
+ DARK_RED_BERRY_1 = Google::Apis::SheetsV4::Color.new(red: 0.659, green: 0.11)
28
+ ORANGE = Google::Apis::SheetsV4::Color.new(red: 1.0, green: 0.6)
29
+ DARK_ORANGE_1 = Google::Apis::SheetsV4::Color.new(red: 0.9, green: 0.569, blue: 0.22)
30
+ YELLOW = Google::Apis::SheetsV4::Color.new(red: 1.0, green: 1.0)
31
+ DARK_YELLOW_1 = Google::Apis::SheetsV4::Color.new(red: 0.945, green: 0.76, blue: 0.196)
32
+ GREEN = Google::Apis::SheetsV4::Color.new(green: 1.0)
33
+ DARK_GREEN_1 = Google::Apis::SheetsV4::Color.new(red: 0.416, green: 0.659, blue: 0.31)
34
+ CYAN = Google::Apis::SheetsV4::Color.new(green: 1.0, blue: 1.0)
35
+ DARK_CYAN_1 = Google::Apis::SheetsV4::Color.new(red: 0.27, green: 0.506, blue: 0.557)
36
+ CORNFLOWER_BLUE = Google::Apis::SheetsV4::Color.new(red: 0.29, green: 0.525, blue: 0.91)
37
+ DARK_CORNFLOWER_BLUE_1 = Google::Apis::SheetsV4::Color.new(red: 0.235, green: 0.47, blue: 0.847)
38
+ BLUE = Google::Apis::SheetsV4::Color.new(blue: 1.0)
39
+ DARK_BLUE_1 = Google::Apis::SheetsV4::Color.new(red: 0.239, green: 0.522, blue: 0.776)
40
+ PURPLE = Google::Apis::SheetsV4::Color.new(red: 0.6, blue: 1.0)
41
+ DARK_PURPLE_1 = Google::Apis::SheetsV4::Color.new(red: 0.404, green: 0.306, blue: 0.655)
42
+ MAGENTA = Google::Apis::SheetsV4::Color.new(red: 1.0, blue: 1.0)
43
+ DARK_MAGENTA_1 = Google::Apis::SheetsV4::Color.new(red: 0.651, green: 0.302, blue: 0.475)
44
+ WHITE = Google::Apis::SheetsV4::Color.new(red: 1.0, green: 1.0, blue: 1.0)
45
+ BLACK = Google::Apis::SheetsV4::Color.new(red: 0.0, green: 0.0, blue: 0.0)
46
+ GRAY = Google::Apis::SheetsV4::Color.new(red: 0.8, green: 0.8, blue: 0.8)
47
+ DARK_GRAY_1 = Google::Apis::SheetsV4::Color.new(red: 0.714, green: 0.714, blue: 0.714)
48
+ end
49
+
19
50
  # @api private
20
51
  # A regexp which matches an invalid character in XML 1.0:
21
52
  # https://en.wikipedia.org/wiki/Valid_characters_in_XML#XML_1.0
@@ -23,44 +54,63 @@ module GoogleDrive
23
54
  /[^\u0009\u000a\u000d\u0020-\ud7ff\ue000-\ufffd\u{10000}-\u{10ffff}]/
24
55
 
25
56
  # @api private
26
- def initialize(session, spreadsheet, worksheet_feed_entry)
57
+ def initialize(session, spreadsheet, properties)
27
58
  @session = session
28
59
  @spreadsheet = spreadsheet
29
- set_worksheet_feed_entry(worksheet_feed_entry)
30
-
60
+ set_properties(properties)
31
61
  @cells = nil
32
62
  @input_values = nil
33
63
  @numeric_values = nil
34
64
  @modified = Set.new
35
65
  @list = nil
66
+ @v4_requests = []
36
67
  end
37
68
 
38
69
  # Nokogiri::XML::Element object of the <entry> element in a worksheets feed.
39
- attr_reader(:worksheet_feed_entry)
70
+ #
71
+ # DEPRECATED: This method is deprecated, and now requires additional
72
+ # network fetch. Consider using properties instead.
73
+ def worksheet_feed_entry
74
+ @worksheet_feed_entry ||= @session.request(:get, worksheet_feed_url).root
75
+ end
76
+
77
+ # Google::Apis::SheetsV4::SheetProperties object for this worksheet.
78
+ attr_reader :properties
40
79
 
41
80
  # Title of the worksheet (shown as tab label in Web interface).
42
- attr_reader(:title)
81
+ attr_reader :title
82
+
83
+ # GoogleDrive::Spreadsheet which this worksheet belongs to.
84
+ attr_reader :spreadsheet
43
85
 
44
86
  # Time object which represents the time the worksheet was last updated.
45
- attr_reader(:updated)
87
+ #
88
+ # DEPRECATED: From google_drive 3.0.0, it returns the time the
89
+ # *spreadsheet* was last updated, instead of the worksheet. This is because
90
+ # it looks the information is not available in Sheets v4 API.
91
+ def updated
92
+ spreadsheet.modified_time.to_time
93
+ end
46
94
 
47
95
  # URL of cell-based feed of the worksheet.
96
+ #
97
+ # DEPRECATED: This method is deprecated, and now requires additional
98
+ # network fetch.
48
99
  def cells_feed_url
49
- @worksheet_feed_entry.css(
100
+ worksheet_feed_entry.css(
50
101
  "link[rel='http://schemas.google.com/spreadsheets/2006#cellsfeed']"
51
102
  )[0]['href']
52
103
  end
53
104
 
54
105
  # URL of worksheet feed URL of the worksheet.
55
106
  def worksheet_feed_url
56
- @worksheet_feed_entry.css("link[rel='self']")[0]['href']
107
+ return '%s/%s' % [spreadsheet.worksheets_feed_url, worksheet_feed_id]
57
108
  end
58
109
 
59
110
  # URL to export the worksheet as CSV.
60
111
  def csv_export_url
61
- @worksheet_feed_entry.css(
62
- "link[rel='http://schemas.google.com/spreadsheets/2006#exportcsv']"
63
- )[0]['href']
112
+ 'https://docs.google.com/spreadsheets/d/%s/export?gid=%s&format=csv' %
113
+ [spreadsheet.id, gid]
64
114
  end
65
115
 
66
116
  # Exports the worksheet as String in CSV format.
@@ -74,10 +124,14 @@ module GoogleDrive
74
124
  open(path, 'wb') { |f| f.write(data) }
75
125
  end
76
126
 
77
- # gid of the worksheet.
127
+ # ID of the worksheet.
128
+ def sheet_id
129
+ @properties.sheet_id
130
+ end
131
+
132
+ # Returns sheet_id.to_s.
78
133
  def gid
79
- # A bit tricky but couldn't find a better way.
80
- CGI.parse(URI.parse(csv_export_url).query)['gid'].last
134
+ sheet_id.to_s
81
135
  end
82
136
 
83
137
  # URL to view/edit the worksheet in a Web browser.
@@ -85,20 +139,6 @@ module GoogleDrive
85
139
  format("%s\#gid=%s", spreadsheet.human_url, gid)
86
140
  end
87
141
 
88
- # GoogleDrive::Spreadsheet which this worksheet belongs to.
89
- def spreadsheet
90
- unless @spreadsheet
91
- unless worksheet_feed_url =~
92
- %r{https?://spreadsheets\.google\.com/feeds/worksheets/(.*)/(.*)$}
93
- raise(GoogleDrive::Error,
94
- 'Worksheet feed URL is in unknown format: ' \
95
- "#{worksheet_feed_url}")
96
- end
97
- @spreadsheet = @session.file_by_id(Regexp.last_match(1))
98
- end
99
- @spreadsheet
100
- end
101
-
102
142
  # Returns content of the cell as String. Arguments must be either
103
143
  # (row number, column number) or cell name. Top-left cell is [1, 1].
104
144
  #
@@ -314,8 +354,19 @@ module GoogleDrive
314
354
  # Note that changes you made by []= etc. is discarded if you haven't called
315
355
  # save().
316
356
  def reload
317
- set_worksheet_feed_entry(@session.request(:get, worksheet_feed_url).root)
318
- reload_cells
357
+ api_spreadsheet =
358
+ @session.sheets_service.get_spreadsheet(
359
+ spreadsheet.id,
360
+ ranges: "'%s'" % @title,
361
+ fields:
362
+ 'sheets(properties,data.rowData.values' \
363
+ '(formattedValue,userEnteredValue,effectiveValue))'
364
+ )
365
+ api_sheet = api_spreadsheet.sheets[0]
366
+ set_properties(api_sheet.properties)
367
+ update_cells_from_api_sheet(api_sheet)
368
+ @v4_requests = []
369
+ @worksheet_feed_entry = nil
319
370
  true
320
371
  end
321
372
 
@@ -324,122 +375,58 @@ module GoogleDrive
324
375
  sent = false
325
376
 
326
377
  if @meta_modified
378
+ add_request({
379
+ update_sheet_properties: {
380
+ properties: {
381
+ sheet_id: sheet_id,
382
+ title: title,
383
+ grid_properties: {row_count: max_rows, column_count: max_cols},
384
+ },
385
+ fields: '*',
386
+ },
387
+ })
388
+ end
327
389
 
328
- edit_url = @worksheet_feed_entry.css("link[rel='edit']")[0]['href']
329
- xml = <<-"EOS"
330
- <entry xmlns='http://www.w3.org/2005/Atom'
331
- xmlns:gs='http://schemas.google.com/spreadsheets/2006'>
332
- <title>#{h(title)}</title>
333
- <gs:rowCount>#{h(max_rows)}</gs:rowCount>
334
- <gs:colCount>#{h(max_cols)}</gs:colCount>
335
- </entry>
336
- EOS
337
-
338
- result = @session.request(
339
- :put,
340
- edit_url,
341
- data: xml,
342
- header: {
343
- 'Content-Type' => 'application/atom+xml;charset=utf-8',
344
- 'If-Match' => '*'
345
- }
346
- )
347
- set_worksheet_feed_entry(result.root)
348
-
390
+ if !@v4_requests.empty?
391
+ self.spreadsheet.batch_update(@v4_requests)
392
+ @v4_requests = []
349
393
  sent = true
350
394
  end
395
+
396
+ @remote_title = @title
351
397
 
352
398
  unless @modified.empty?
353
- # Gets id and edit URL for each cell.
354
- # Note that return-empty=true is required to get those info for empty cells.
355
- cell_entries = {}
356
- rows = @modified.map { |r, _c| r }
357
- cols = @modified.map { |_r, c| c }
358
- url = concat_url(
359
- cells_feed_url,
360
- "?return-empty=true&min-row=#{rows.min}&max-row=#{rows.max}" \
361
- "&min-col=#{cols.min}&max-col=#{cols.max}"
362
- )
363
- doc = @session.request(:get, url)
364
-
365
- doc.css('entry').each do |entry|
366
- row = entry.css('gs|cell')[0]['row'].to_i
367
- col = entry.css('gs|cell')[0]['col'].to_i
368
- cell_entries[[row, col]] = entry
369
- end
370
-
371
- xml = <<-EOS
372
- <feed xmlns="http://www.w3.org/2005/Atom"
373
- xmlns:batch="http://schemas.google.com/gdata/batch"
374
- xmlns:gs="http://schemas.google.com/spreadsheets/2006">
375
- <id>#{h(cells_feed_url)}</id>
376
- EOS
377
- @modified.each do |row, col|
378
- value = @cells[[row, col]]
379
- entry = cell_entries[[row, col]]
380
- id = entry.css('id').text
381
- edit_link = entry.css("link[rel='edit']")[0]
382
- unless edit_link
383
- raise(
384
- GoogleDrive::Error,
385
- format(
386
- "The user doesn't have write permission to the spreadsheet: %p",
387
- spreadsheet
388
- )
389
- )
390
- end
391
- edit_url = edit_link['href']
392
- xml << <<-EOS
393
- <entry>
394
- <batch:id>#{h(row)},#{h(col)}</batch:id>
395
- <batch:operation type="update"/>
396
- <id>#{h(id)}</id>
397
- <link
398
- rel="edit"
399
- type="application/atom+xml"
400
- href="#{h(edit_url)}"/>
401
- <gs:cell
402
- row="#{h(row)}"
403
- col="#{h(col)}"
404
- inputValue="#{h(value)}"/>
405
- </entry>
406
- EOS
399
+ min_modified_row = 1.0 / 0.0
400
+ max_modified_row = 0
401
+ min_modified_col = 1.0 / 0.0
402
+ max_modified_col = 0
403
+ @modified.each do |r, c|
404
+ min_modified_row = r if r < min_modified_row
405
+ max_modified_row = r if r > max_modified_row
406
+ min_modified_col = c if c < min_modified_col
407
+ max_modified_col = c if c > max_modified_col
407
408
  end
408
- xml << <<-"EOS"
409
- </feed>
410
- EOS
411
-
412
- batch_url = concat_url(cells_feed_url, '/batch')
413
- result = @session.request(
414
- :post,
415
- batch_url,
416
- data: xml,
417
- header: {
418
- 'Content-Type' => 'application/atom+xml;charset=utf-8',
419
- 'If-Match' => '*'
420
- }
421
- )
422
- result.css('entry').each do |entry|
423
- interrupted = entry.css('batch|interrupted')[0]
424
- if interrupted
425
- raise(
426
- GoogleDrive::Error,
427
- format('Update has failed: %s', interrupted['reason'])
428
- )
409
+
410
+ # Uses update_spreadsheet_value instead batch_update_spreadsheet with
411
+ # update_cells. batch_update_spreadsheet has benefit that the request
412
+ # can be batched with other requests. But it has drawback that the
413
+ # type of the value (string_value, number_value, etc.) must be
414
+ # explicitly specified in user_entered_value. Since I don't know exact
415
+ # logic to determine the type from text, I chose to use
416
+ # update_spreadsheet_value here.
417
+ range = "'%s'!R%dC%d:R%dC%d" %
418
+ [@title, min_modified_row, min_modified_col, max_modified_row, max_modified_col]
419
+ values = (min_modified_row..max_modified_row).map do |r|
420
+ (min_modified_col..max_modified_col).map do |c|
421
+ @modified.include?([r, c]) ? (@cells[[r, c]] || '') : nil
429
422
  end
430
- next if entry.css('batch|status').first['code'] =~ /^2/
431
- raise(
432
- GoogleDrive::Error,
433
- format(
434
- 'Updating cell %s has failed: %s',
435
- entry.css('id').text, entry.css('batch|status')[0]['reason']
436
- )
437
- )
438
423
  end
424
+ value_range = Google::Apis::SheetsV4::ValueRange.new(values: values)
425
+ @session.sheets_service.update_spreadsheet_value(
426
+ spreadsheet.id, range, value_range, value_input_option: 'USER_ENTERED')
439
427
 
440
428
  @modified.clear
441
429
  sent = true
442
-
443
430
  end
444
431
 
445
432
  sent
@@ -454,17 +441,20 @@ module GoogleDrive
454
441
  # Deletes this worksheet. Deletion takes effect right away without calling
455
442
  # save().
456
443
  def delete
457
- ws_doc = @session.request(:get, worksheet_feed_url)
458
- edit_url = ws_doc.css("link[rel='edit']")[0]['href']
459
- @session.request(:delete, edit_url)
444
+ spreadsheet.batch_update([{
445
+ delete_sheet: Google::Apis::SheetsV4::DeleteSheetRequest.new(sheet_id: sheet_id),
446
+ }])
460
447
  end
461
448
 
462
- # Returns true if you have changes made by []= which haven't been saved.
449
+ # Returns true if you have changes made by []= etc. which haven't been saved.
463
450
  def dirty?
464
- !@modified.empty?
451
+ !@modified.empty? || !@v4_requests.empty?
465
452
  end
466
453
 
467
454
  # List feed URL of the worksheet.
455
+ #
456
+ # DEPRECATED: This method is deprecated, and now requires additional
457
+ # network fetch.
468
458
  def list_feed_url
469
459
  @worksheet_feed_entry.css(
470
460
  "link[rel='http://schemas.google.com/spreadsheets/2006#listfeed']"
@@ -517,7 +507,7 @@ module GoogleDrive
517
507
  end
518
508
 
519
509
  def inspect
520
- fields = { worksheet_feed_url: worksheet_feed_url }
510
+ fields = { spreadsheet_id: spreadsheet.id, gid: gid }
521
511
  fields[:title] = @title if @title
522
512
  format(
523
513
  "\#<%p %s>",
@@ -526,39 +516,165 @@ module GoogleDrive
526
516
  )
527
517
  end
528
518
 
529
- private
519
+ # Merges a range of cells together. "MERGE_COLUMNS" is another option for merge_type
520
+ def merge_cells(top_row, left_col, num_rows, num_cols, merge_type: 'MERGE_ALL')
521
+ range = v4_range_object(top_row, left_col, num_rows, num_cols)
522
+ add_request({
523
+ merge_cells:
524
+ Google::Apis::SheetsV4::MergeCellsRequest.new(
525
+ range: range, merge_type: merge_type),
526
+ })
527
+ end
528
+
529
+ # Changes the formatting of a range of cells to match the given number format.
530
+ # For example to change A1 to a percentage with 1 decimal point:
531
+ # worksheet.set_number_format(1, 1, 1, 1, "##.#%")
532
+ # Google API reference: https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets#numberformat
533
+ def set_number_format(top_row, left_col, num_rows, num_cols, pattern, type: "NUMBER")
534
+ number_format = Google::Apis::SheetsV4::NumberFormat.new(type: type, pattern: pattern)
535
+ format = Google::Apis::SheetsV4::CellFormat.new(number_format: number_format)
536
+ fields = 'userEnteredFormat(numberFormat)'
537
+ format_cells(top_row, left_col, num_rows, num_cols, format, fields)
538
+ end
539
+
540
+ # Changes text alignment of a range of cells.
541
+ # Horizontal alignment can be "LEFT", "CENTER", or "RIGHT".
542
+ # Vertical alignment can be "TOP", "MIDDLE", or "BOTTOM".
543
+ # Google API reference: https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets#HorizontalAlign
544
+ def set_text_alignment(
545
+ top_row, left_col, num_rows, num_cols,
546
+ horizontal: nil, vertical: nil)
547
+ return if horizontal.nil? && vertical.nil?
548
+
549
+ format = Google::Apis::SheetsV4::CellFormat.new(
550
+ horizontal_alignment: horizontal, vertical_alignment: vertical)
551
+ subfields =
552
+ (horizontal.nil? ? [] : ['horizontalAlignment']) +
553
+ (vertical.nil? ? [] : ['verticalAlignment'])
554
+
555
+ fields = 'userEnteredFormat(%s)' % subfields.join(',')
556
+ format_cells(top_row, left_col, num_rows, num_cols, format, fields)
557
+ end
558
+
559
+ # Changes the background color on a range of cells. e.g.:
560
+ # worksheet.set_background_color(1, 1, 1, 1, GoogleDrive::Worksheet::Colors::DARK_YELLOW_1)
561
+ #
562
+ # background_color is an instance of Google::Apis::SheetsV4::Color.
563
+ def set_background_color(top_row, left_col, num_rows, num_cols, background_color)
564
+ format = Google::Apis::SheetsV4::CellFormat.new(background_color: background_color)
565
+ fields = 'userEnteredFormat(backgroundColor)'
566
+ format_cells(top_row, left_col, num_rows, num_cols, format, fields)
567
+ end
568
+
569
+ # Change the text formatting on a range of cells. e.g., To set cell
570
+ # A1 to have red text that is bold and italic:
571
+ # worksheet.set_text_format(
572
+ # 1, 1, 1, 1,
573
+ # bold: true,
574
+ # italic: true,
575
+ # foreground_color: GoogleDrive::Worksheet::Colors::RED_BERRY)
576
+ #
577
+ # foreground_color is an instance of Google::Apis::SheetsV4::Color.
578
+ # Google API reference:
579
+ # https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets#textformat
580
+ def set_text_format(top_row, left_col, num_rows, num_cols, bold: false,
581
+ italic: false, strikethrough: false, font_size: nil,
582
+ font_family: nil, foreground_color: nil)
583
+ text_format = Google::Apis::SheetsV4::TextFormat.new(
584
+ bold: bold,
585
+ italic: italic,
586
+ strikethrough: strikethrough,
587
+ font_size: font_size,
588
+ font_family: font_family,
589
+ foreground_color: foreground_color
590
+ )
530
591
 
531
- def set_worksheet_feed_entry(entry)
532
- @worksheet_feed_entry = entry
533
- @title = entry.css('title').text
534
- set_max_values(entry)
535
- @updated = Time.parse(entry.css('updated').text)
536
- @meta_modified = false
592
+ format = Google::Apis::SheetsV4::CellFormat.new(text_format: text_format)
593
+ fields = 'userEnteredFormat(textFormat)'
594
+ format_cells(top_row, left_col, num_rows, num_cols, format, fields)
595
+ end
596
+
597
+ # Update the border styles for a range of cells.
598
+ # borders is a Hash of Google::Apis::SheetsV4::Border keyed with the
599
+ # following symbols: :top, :bottom, :left, :right, :innerHorizontal, :innerVertical
600
+ # e.g., To set a black double-line on the bottom of A1:
601
+ # update_borders(
602
+ # 1, 1, 1, 1,
603
+ # {bottom: Google::Apis::SheetsV4::Border.new(
604
+ # style: "DOUBLE", color: GoogleDrive::Worksheet::Colors::BLACK)})
605
+ def update_borders(top_row, left_col, num_rows, num_cols, borders)
606
+ request = Google::Apis::SheetsV4::UpdateBordersRequest.new(borders)
607
+ request.range = v4_range_object(top_row, left_col, num_rows, num_cols)
608
+ add_request({update_borders: request})
609
+ end
610
+
611
+ # Add an instance of Google::Apis::SheetsV4::Request (or its Hash
612
+ # equivalent) which will be applied on the next call to the save method.
613
+ def add_request(request)
614
+ @v4_requests.push(request)
537
615
  end
538
616
 
539
- def set_max_values(entry)
540
- @max_rows = entry.css('gs|rowCount').text.to_i
541
- @max_cols = entry.css('gs|colCount').text.to_i
617
+ # @api private
618
+ def worksheet_feed_id
619
+ gid_int = sheet_id
620
+ xor_val = gid_int > 31578 ? 474 : 31578
621
+ letter = gid_int > 31578 ? 'o' : ''
622
+ letter + (gid_int ^ xor_val).to_s(36)
623
+ end
624
+
625
+ private
626
+
627
+ def format_cells(top_row, left_col, num_rows, num_cols, format, fields)
628
+ add_request({
629
+ repeat_cell:
630
+ Google::Apis::SheetsV4::RepeatCellRequest.new(
631
+ range: v4_range_object(top_row, left_col, num_rows, num_cols),
632
+ cell: Google::Apis::SheetsV4::CellData.new(user_entered_format: format),
633
+ fields: fields
634
+ ),
635
+ })
636
+ end
637
+
638
+ def set_properties(properties)
639
+ @properties = properties
640
+ @title = @remote_title = properties.title
641
+ @max_rows = properties.grid_properties.row_count
642
+ @max_cols = properties.grid_properties.column_count
643
+ @meta_modified = false
542
644
  end
543
645
 
544
646
  def reload_cells
545
- doc = @session.request(:get, cells_feed_url)
647
+ response =
648
+ @session.sheets_service.get_spreadsheet(
649
+ spreadsheet.id,
650
+ ranges: "'%s'" % @remote_title,
651
+ fields: 'sheets.data.rowData.values(formattedValue,userEnteredValue,effectiveValue)'
652
+ )
653
+ update_cells_from_api_sheet(response.sheets[0])
654
+ end
546
655
 
547
- @num_cols = nil
548
- @num_rows = nil
656
+ def update_cells_from_api_sheet(api_sheet)
657
+ rows_data = api_sheet.data[0].row_data || []
549
658
 
659
+ @num_rows = rows_data.size
660
+ @num_cols = 0
550
661
  @cells = {}
551
662
  @input_values = {}
552
663
  @numeric_values = {}
553
- doc.css('feed > entry').each do |entry|
554
- cell = entry.css('gs|cell')[0]
555
- row = cell['row'].to_i
556
- col = cell['col'].to_i
557
- @cells[[row, col]] = cell.inner_text
558
- @input_values[[row, col]] = cell['inputValue'] || cell.inner_text
559
- numeric_value = cell['numericValue']
560
- @numeric_values[[row, col]] = numeric_value ? numeric_value.to_f : nil
664
+
665
+ rows_data.each_with_index do |row_data, r|
666
+ next if !row_data.values
667
+ @num_cols = row_data.values.size if row_data.values.size > @num_cols
668
+ row_data.values.each_with_index do |cell_data, c|
669
+ k = [r + 1, c + 1]
670
+ @cells[k] = cell_data.formatted_value || ''
671
+ @input_values[k] = extended_value_to_str(cell_data.user_entered_value)
672
+ @numeric_values[k] =
673
+ cell_data.effective_value && cell_data.effective_value.number_value ?
674
+ cell_data.effective_value.number_value.to_f : nil
675
+ end
561
676
  end
677
+
562
678
  @modified.clear
563
679
  end
564
680
 
@@ -596,5 +712,25 @@ module GoogleDrive
596
712
  )
597
713
  end
598
714
  end
715
+
716
+ def v4_range_object(top_row, left_col, num_rows, num_cols)
717
+ Google::Apis::SheetsV4::GridRange.new(
718
+ sheet_id: sheet_id,
719
+ start_row_index: top_row - 1,
720
+ start_column_index: left_col - 1,
721
+ end_row_index: top_row + num_rows - 1,
722
+ end_column_index: left_col + num_cols - 1
723
+ )
724
+ end
725
+
726
+ def extended_value_to_str(extended_value)
727
+ return '' if !extended_value
728
+ value =
729
+ extended_value.number_value ||
730
+ extended_value.string_value ||
731
+ extended_value.bool_value ||
732
+ extended_value.formula_value
733
+ value.to_s
734
+ end
599
735
  end
600
736
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: google_drive
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.12
4
+ version: 3.0.0.pre1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Hiroshi Ichikawa
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-09-10 00:00:00.000000000 Z
11
+ date: 2018-11-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: nokogiri
@@ -164,9 +164,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
164
164
  version: 2.0.0
165
165
  required_rubygems_version: !ruby/object:Gem::Requirement
166
166
  requirements:
167
- - - ">="
167
+ - - ">"
168
168
  - !ruby/object:Gem::Version
169
- version: '0'
169
+ version: 1.3.1
170
170
  requirements: []
171
171
  rubyforge_project:
172
172
  rubygems_version: 2.6.14