google_drive2 3.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.
- checksums.yaml +7 -0
- data/README.md +136 -0
- data/lib/google_drive/access_token_credentials.rb +19 -0
- data/lib/google_drive/acl.rb +111 -0
- data/lib/google_drive/acl_entry.rb +181 -0
- data/lib/google_drive/api_client_fetcher.rb +59 -0
- data/lib/google_drive/authentication_error.rb +10 -0
- data/lib/google_drive/collection.rb +207 -0
- data/lib/google_drive/config.rb +36 -0
- data/lib/google_drive/error.rb +8 -0
- data/lib/google_drive/file.rb +266 -0
- data/lib/google_drive/list.rb +125 -0
- data/lib/google_drive/list_row.rb +89 -0
- data/lib/google_drive/response_code_error.rb +23 -0
- data/lib/google_drive/session.rb +733 -0
- data/lib/google_drive/spreadsheet.rb +135 -0
- data/lib/google_drive/util.rb +243 -0
- data/lib/google_drive/worksheet.rb +770 -0
- data/lib/google_drive.rb +35 -0
- metadata +196 -0
@@ -0,0 +1,770 @@
|
|
1
|
+
# Author: Hiroshi Ichikawa <http://gimite.net/>
|
2
|
+
# The license of this source is "New BSD Licence"
|
3
|
+
|
4
|
+
require 'cgi'
|
5
|
+
require 'set'
|
6
|
+
require 'uri'
|
7
|
+
|
8
|
+
require 'google_drive/util'
|
9
|
+
require 'google_drive/error'
|
10
|
+
require 'google_drive/list'
|
11
|
+
|
12
|
+
module GoogleDrive
|
13
|
+
# A worksheet (i.e. a tab) in a spreadsheet.
|
14
|
+
# Use GoogleDrive::Spreadsheet#worksheets to get GoogleDrive::Worksheet
|
15
|
+
# object.
|
16
|
+
class Worksheet
|
17
|
+
include(Util)
|
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
|
+
|
50
|
+
# @api private
|
51
|
+
# A regexp which matches an invalid character in XML 1.0:
|
52
|
+
# https://en.wikipedia.org/wiki/Valid_characters_in_XML#XML_1.0
|
53
|
+
XML_INVALID_CHAR_REGEXP =
|
54
|
+
/[^\u0009\u000a\u000d\u0020-\ud7ff\ue000-\ufffd\u{10000}-\u{10ffff}]/
|
55
|
+
|
56
|
+
# @api private
|
57
|
+
def initialize(session, spreadsheet, properties)
|
58
|
+
@session = session
|
59
|
+
@spreadsheet = spreadsheet
|
60
|
+
set_properties(properties)
|
61
|
+
@cells = nil
|
62
|
+
@input_values = nil
|
63
|
+
@numeric_values = nil
|
64
|
+
@modified = Set.new
|
65
|
+
@list = nil
|
66
|
+
@v4_requests = []
|
67
|
+
end
|
68
|
+
|
69
|
+
# Nokogiri::XML::Element object of the <entry> element in a worksheets feed.
|
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
|
79
|
+
|
80
|
+
# Title of the worksheet (shown as tab label in Web interface).
|
81
|
+
attr_reader :title
|
82
|
+
|
83
|
+
# Index of the worksheet (affects tab order in web interface).
|
84
|
+
attr_reader :index
|
85
|
+
|
86
|
+
# GoogleDrive::Spreadsheet which this worksheet belongs to.
|
87
|
+
attr_reader :spreadsheet
|
88
|
+
|
89
|
+
# Time object which represents the time the worksheet was last updated.
|
90
|
+
#
|
91
|
+
# DEPRECATED: From google_drive 3.0.0, it returns the time the
|
92
|
+
# *spreadsheet* was last updated, instead of the worksheet. This is because
|
93
|
+
# it looks the information is not available in Sheets v4 API.
|
94
|
+
def updated
|
95
|
+
spreadsheet.modified_time.to_time
|
96
|
+
end
|
97
|
+
|
98
|
+
# URL of cell-based feed of the worksheet.
|
99
|
+
#
|
100
|
+
# DEPRECATED: This method is deprecated, and now requires additional
|
101
|
+
# network fetch.
|
102
|
+
def cells_feed_url
|
103
|
+
worksheet_feed_entry.css(
|
104
|
+
"link[rel='http://schemas.google.com/spreadsheets/2006#cellsfeed']"
|
105
|
+
)[0]['href']
|
106
|
+
end
|
107
|
+
|
108
|
+
# URL of worksheet feed URL of the worksheet.
|
109
|
+
def worksheet_feed_url
|
110
|
+
return '%s/%s' % [spreadsheet.worksheets_feed_url, worksheet_feed_id]
|
111
|
+
end
|
112
|
+
|
113
|
+
# URL to export the worksheet as CSV.
|
114
|
+
def csv_export_url
|
115
|
+
'https://docs.google.com/spreadsheets/d/%s/export?gid=%s&format=csv' %
|
116
|
+
[spreadsheet.id, gid]
|
117
|
+
end
|
118
|
+
|
119
|
+
# Exports the worksheet as String in CSV format.
|
120
|
+
def export_as_string
|
121
|
+
@session.request(:get, csv_export_url, response_type: :raw)
|
122
|
+
end
|
123
|
+
|
124
|
+
# Exports the worksheet to +path+ in CSV format.
|
125
|
+
def export_as_file(path)
|
126
|
+
data = export_as_string
|
127
|
+
open(path, 'wb') { |f| f.write(data) }
|
128
|
+
end
|
129
|
+
|
130
|
+
# ID of the worksheet.
|
131
|
+
def sheet_id
|
132
|
+
@properties.sheet_id
|
133
|
+
end
|
134
|
+
|
135
|
+
# Returns sheet_id.to_s.
|
136
|
+
def gid
|
137
|
+
sheet_id.to_s
|
138
|
+
end
|
139
|
+
|
140
|
+
# URL to view/edit the worksheet in a Web browser.
|
141
|
+
def human_url
|
142
|
+
format("%s\#gid=%s", spreadsheet.human_url, gid)
|
143
|
+
end
|
144
|
+
|
145
|
+
# Copy worksheet to specified spreadsheet.
|
146
|
+
# This method can take either instance of GoogleDrive::Spreadsheet or its id.
|
147
|
+
def copy_to(spreadsheet_or_id)
|
148
|
+
destination_spreadsheet_id =
|
149
|
+
spreadsheet_or_id.respond_to?(:id) ?
|
150
|
+
spreadsheet_or_id.id : spreadsheet_or_id
|
151
|
+
request = Google::Apis::SheetsV4::CopySheetToAnotherSpreadsheetRequest.new(
|
152
|
+
destination_spreadsheet_id: destination_spreadsheet_id,
|
153
|
+
)
|
154
|
+
@session.sheets_service.copy_spreadsheet(spreadsheet.id, sheet_id, request)
|
155
|
+
nil
|
156
|
+
end
|
157
|
+
|
158
|
+
# Copy worksheet to owner spreadsheet.
|
159
|
+
def duplicate
|
160
|
+
copy_to(spreadsheet)
|
161
|
+
end
|
162
|
+
|
163
|
+
# Returns content of the cell as String. Arguments must be either
|
164
|
+
# (row number, column number) or cell name. Top-left cell is [1, 1].
|
165
|
+
#
|
166
|
+
# e.g.
|
167
|
+
# worksheet[2, 1] #=> "hoge"
|
168
|
+
# worksheet["A2"] #=> "hoge"
|
169
|
+
def [](*args)
|
170
|
+
(row, col) = parse_cell_args(args)
|
171
|
+
cells[[row, col]] || ''
|
172
|
+
end
|
173
|
+
|
174
|
+
# Updates content of the cell.
|
175
|
+
# Arguments in the bracket must be either (row number, column number) or
|
176
|
+
# cell name. Note that update is not sent to the server until you call
|
177
|
+
# save().
|
178
|
+
# Top-left cell is [1, 1].
|
179
|
+
#
|
180
|
+
# e.g.
|
181
|
+
# worksheet[2, 1] = "hoge"
|
182
|
+
# worksheet["A2"] = "hoge"
|
183
|
+
# worksheet[1, 3] = "=A1+B1"
|
184
|
+
def []=(*args)
|
185
|
+
(row, col) = parse_cell_args(args[0...-1])
|
186
|
+
value = args[-1].to_s
|
187
|
+
validate_cell_value(value)
|
188
|
+
reload_cells unless @cells
|
189
|
+
@cells[[row, col]] = value
|
190
|
+
@input_values[[row, col]] = value
|
191
|
+
@numeric_values[[row, col]] = nil
|
192
|
+
@modified.add([row, col])
|
193
|
+
self.max_rows = row if row > @max_rows
|
194
|
+
self.max_cols = col if col > @max_cols
|
195
|
+
if value.empty?
|
196
|
+
@num_rows = nil
|
197
|
+
@num_cols = nil
|
198
|
+
else
|
199
|
+
@num_rows = row if @num_rows && row > @num_rows
|
200
|
+
@num_cols = col if @num_cols && col > @num_cols
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
# Updates cells in a rectangle area by a two-dimensional Array.
|
205
|
+
# +top_row+ and +left_col+ specifies the top-left corner of the area.
|
206
|
+
#
|
207
|
+
# e.g.
|
208
|
+
# worksheet.update_cells(2, 3, [["1", "2"], ["3", "4"]])
|
209
|
+
def update_cells(top_row, left_col, darray)
|
210
|
+
darray.each_with_index do |array, y|
|
211
|
+
array.each_with_index do |value, x|
|
212
|
+
self[top_row + y, left_col + x] = value
|
213
|
+
end
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
# Returns the value or the formula of the cell. Arguments must be either
|
218
|
+
# (row number, column number) or cell name. Top-left cell is [1, 1].
|
219
|
+
#
|
220
|
+
# If user input "=A1+B1" to cell [1, 3]:
|
221
|
+
# worksheet[1, 3] #=> "3" for example
|
222
|
+
# worksheet.input_value(1, 3) #=> "=RC[-2]+RC[-1]"
|
223
|
+
def input_value(*args)
|
224
|
+
(row, col) = parse_cell_args(args)
|
225
|
+
reload_cells unless @cells
|
226
|
+
@input_values[[row, col]] || ''
|
227
|
+
end
|
228
|
+
|
229
|
+
# Returns the numeric value of the cell. Arguments must be either
|
230
|
+
# (row number, column number) or cell name. Top-left cell is [1, 1].
|
231
|
+
#
|
232
|
+
# e.g.
|
233
|
+
# worksheet[1, 3]
|
234
|
+
# #=> "3,0" # it depends on locale, currency...
|
235
|
+
# worksheet.numeric_value(1, 3)
|
236
|
+
# #=> 3.0
|
237
|
+
#
|
238
|
+
# Returns nil if the cell is empty or contains non-number.
|
239
|
+
#
|
240
|
+
# If you modify the cell, its numeric_value is nil until you call save()
|
241
|
+
# and reload().
|
242
|
+
#
|
243
|
+
# For details, see:
|
244
|
+
# https://developers.google.com/google-apps/spreadsheets/#working_with_cell-based_feeds
|
245
|
+
def numeric_value(*args)
|
246
|
+
(row, col) = parse_cell_args(args)
|
247
|
+
reload_cells unless @cells
|
248
|
+
@numeric_values[[row, col]]
|
249
|
+
end
|
250
|
+
|
251
|
+
# Row number of the bottom-most non-empty row.
|
252
|
+
def num_rows
|
253
|
+
reload_cells unless @cells
|
254
|
+
# Memoizes it because this can be bottle-neck.
|
255
|
+
# https://github.com/gimite/google-drive-ruby/pull/49
|
256
|
+
@num_rows ||=
|
257
|
+
@input_values
|
258
|
+
.reject { |(_r, _c), v| v.empty? }
|
259
|
+
.map { |(r, _c), _v| r }
|
260
|
+
.max ||
|
261
|
+
0
|
262
|
+
end
|
263
|
+
|
264
|
+
# Column number of the right-most non-empty column.
|
265
|
+
def num_cols
|
266
|
+
reload_cells unless @cells
|
267
|
+
# Memoizes it because this can be bottle-neck.
|
268
|
+
# https://github.com/gimite/google-drive-ruby/pull/49
|
269
|
+
@num_cols ||=
|
270
|
+
@input_values
|
271
|
+
.reject { |(_r, _c), v| v.empty? }
|
272
|
+
.map { |(_r, c), _v| c }
|
273
|
+
.max ||
|
274
|
+
0
|
275
|
+
end
|
276
|
+
|
277
|
+
# Number of rows including empty rows.
|
278
|
+
attr_reader :max_rows
|
279
|
+
|
280
|
+
# Updates number of rows.
|
281
|
+
# Note that update is not sent to the server until you call save().
|
282
|
+
def max_rows=(rows)
|
283
|
+
@max_rows = rows
|
284
|
+
@meta_modified = true
|
285
|
+
end
|
286
|
+
|
287
|
+
# Number of columns including empty columns.
|
288
|
+
attr_reader :max_cols
|
289
|
+
|
290
|
+
# Updates number of columns.
|
291
|
+
# Note that update is not sent to the server until you call save().
|
292
|
+
def max_cols=(cols)
|
293
|
+
@max_cols = cols
|
294
|
+
@meta_modified = true
|
295
|
+
end
|
296
|
+
|
297
|
+
# Updates title of the worksheet.
|
298
|
+
# Note that update is not sent to the server until you call save().
|
299
|
+
def title=(title)
|
300
|
+
@title = title
|
301
|
+
@meta_modified = true
|
302
|
+
end
|
303
|
+
|
304
|
+
# Updates index of the worksheet.
|
305
|
+
# Note that update is not sent to the server until you call save().
|
306
|
+
def index=(index)
|
307
|
+
@index = index
|
308
|
+
@meta_modified = true
|
309
|
+
end
|
310
|
+
|
311
|
+
# @api private
|
312
|
+
def cells
|
313
|
+
reload_cells unless @cells
|
314
|
+
@cells
|
315
|
+
end
|
316
|
+
|
317
|
+
# An array of spreadsheet rows. Each row contains an array of
|
318
|
+
# columns. Note that resulting array is 0-origin so:
|
319
|
+
#
|
320
|
+
# worksheet.rows[0][0] == worksheet[1, 1]
|
321
|
+
def rows(skip = 0)
|
322
|
+
nc = num_cols
|
323
|
+
result = ((1 + skip)..num_rows).map do |row|
|
324
|
+
(1..nc).map { |col| self[row, col] }.freeze
|
325
|
+
end
|
326
|
+
result.freeze
|
327
|
+
end
|
328
|
+
|
329
|
+
# Inserts rows.
|
330
|
+
#
|
331
|
+
# e.g.
|
332
|
+
# # Inserts 2 empty rows before row 3.
|
333
|
+
# worksheet.insert_rows(3, 2)
|
334
|
+
# # Inserts 2 rows with values before row 3.
|
335
|
+
# worksheet.insert_rows(3, [["a, "b"], ["c, "d"]])
|
336
|
+
#
|
337
|
+
# Note that this method is implemented by shifting all cells below the row.
|
338
|
+
# Its behavior is different from inserting rows on the web interface if the
|
339
|
+
# worksheet contains inter-cell reference.
|
340
|
+
def insert_rows(row_num, rows)
|
341
|
+
rows = Array.new(rows, []) if rows.is_a?(Integer)
|
342
|
+
|
343
|
+
# Shifts all cells below the row.
|
344
|
+
self.max_rows += rows.size
|
345
|
+
num_rows.downto(row_num) do |r|
|
346
|
+
(1..num_cols).each do |c|
|
347
|
+
self[r + rows.size, c] = input_value(r, c)
|
348
|
+
end
|
349
|
+
end
|
350
|
+
|
351
|
+
# Fills in the inserted rows.
|
352
|
+
num_cols = self.num_cols
|
353
|
+
rows.each_with_index do |row, r|
|
354
|
+
(0...[row.size, num_cols].max).each do |c|
|
355
|
+
self[row_num + r, 1 + c] = row[c] || ''
|
356
|
+
end
|
357
|
+
end
|
358
|
+
end
|
359
|
+
|
360
|
+
# Deletes rows.
|
361
|
+
#
|
362
|
+
# e.g.
|
363
|
+
# # Deletes 2 rows starting from row 3 (i.e., deletes row 3 and 4).
|
364
|
+
# worksheet.delete_rows(3, 2)
|
365
|
+
#
|
366
|
+
# Note that this method is implemented by shifting all cells below the row.
|
367
|
+
# Its behavior is different from deleting rows on the web interface if the
|
368
|
+
# worksheet contains inter-cell reference.
|
369
|
+
def delete_rows(row_num, rows)
|
370
|
+
if row_num + rows - 1 > self.max_rows
|
371
|
+
raise(ArgumentError, 'The row number is out of range')
|
372
|
+
end
|
373
|
+
for r in row_num..(self.max_rows - rows)
|
374
|
+
for c in 1..num_cols
|
375
|
+
self[r, c] = input_value(r + rows, c)
|
376
|
+
end
|
377
|
+
end
|
378
|
+
self.max_rows -= rows
|
379
|
+
end
|
380
|
+
|
381
|
+
# Reloads content of the worksheets from the server.
|
382
|
+
# Note that changes you made by []= etc. is discarded if you haven't called
|
383
|
+
# save().
|
384
|
+
def reload
|
385
|
+
api_spreadsheet =
|
386
|
+
@session.sheets_service.get_spreadsheet(
|
387
|
+
spreadsheet.id,
|
388
|
+
ranges: "'%s'" % @title,
|
389
|
+
fields:
|
390
|
+
'sheets(properties,data.rowData.values' \
|
391
|
+
'(formattedValue,userEnteredValue,effectiveValue))'
|
392
|
+
)
|
393
|
+
api_sheet = api_spreadsheet.sheets[0]
|
394
|
+
set_properties(api_sheet.properties)
|
395
|
+
update_cells_from_api_sheet(api_sheet)
|
396
|
+
@v4_requests = []
|
397
|
+
@worksheet_feed_entry = nil
|
398
|
+
true
|
399
|
+
end
|
400
|
+
|
401
|
+
# Saves your changes made by []=, etc. to the server.
|
402
|
+
def save
|
403
|
+
sent = false
|
404
|
+
|
405
|
+
if @meta_modified
|
406
|
+
add_request({
|
407
|
+
update_sheet_properties: {
|
408
|
+
properties: {
|
409
|
+
sheet_id: sheet_id,
|
410
|
+
title: title,
|
411
|
+
index: index,
|
412
|
+
grid_properties: {row_count: max_rows, column_count: max_cols},
|
413
|
+
},
|
414
|
+
fields: '*',
|
415
|
+
},
|
416
|
+
})
|
417
|
+
end
|
418
|
+
|
419
|
+
if !@v4_requests.empty?
|
420
|
+
self.spreadsheet.batch_update(@v4_requests)
|
421
|
+
@v4_requests = []
|
422
|
+
sent = true
|
423
|
+
end
|
424
|
+
|
425
|
+
@remote_title = @title
|
426
|
+
|
427
|
+
unless @modified.empty?
|
428
|
+
min_modified_row = 1.0 / 0.0
|
429
|
+
max_modified_row = 0
|
430
|
+
min_modified_col = 1.0 / 0.0
|
431
|
+
max_modified_col = 0
|
432
|
+
@modified.each do |r, c|
|
433
|
+
min_modified_row = r if r < min_modified_row
|
434
|
+
max_modified_row = r if r > max_modified_row
|
435
|
+
min_modified_col = c if c < min_modified_col
|
436
|
+
max_modified_col = c if c > max_modified_col
|
437
|
+
end
|
438
|
+
|
439
|
+
# Uses update_spreadsheet_value instead batch_update_spreadsheet with
|
440
|
+
# update_cells. batch_update_spreadsheet has benefit that the request
|
441
|
+
# can be batched with other requests. But it has drawback that the
|
442
|
+
# type of the value (string_value, number_value, etc.) must be
|
443
|
+
# explicitly specified in user_entered_value. Since I don't know exact
|
444
|
+
# logic to determine the type from text, I chose to use
|
445
|
+
# update_spreadsheet_value here.
|
446
|
+
range = "'%s'!R%dC%d:R%dC%d" %
|
447
|
+
[@title, min_modified_row, min_modified_col, max_modified_row, max_modified_col]
|
448
|
+
values = (min_modified_row..max_modified_row).map do |r|
|
449
|
+
(min_modified_col..max_modified_col).map do |c|
|
450
|
+
@modified.include?([r, c]) ? (@cells[[r, c]] || '') : nil
|
451
|
+
end
|
452
|
+
end
|
453
|
+
value_range = Google::Apis::SheetsV4::ValueRange.new(values: values)
|
454
|
+
@session.sheets_service.update_spreadsheet_value(
|
455
|
+
spreadsheet.id, range, value_range, value_input_option: 'USER_ENTERED')
|
456
|
+
|
457
|
+
@modified.clear
|
458
|
+
sent = true
|
459
|
+
end
|
460
|
+
|
461
|
+
sent
|
462
|
+
end
|
463
|
+
|
464
|
+
# Calls save() and reload().
|
465
|
+
def synchronize
|
466
|
+
save
|
467
|
+
reload
|
468
|
+
end
|
469
|
+
|
470
|
+
# Deletes this worksheet. Deletion takes effect right away without calling
|
471
|
+
# save().
|
472
|
+
def delete
|
473
|
+
spreadsheet.batch_update([{
|
474
|
+
delete_sheet: Google::Apis::SheetsV4::DeleteSheetRequest.new(sheet_id: sheet_id),
|
475
|
+
}])
|
476
|
+
end
|
477
|
+
|
478
|
+
# Returns true if you have changes made by []= etc. which haven't been saved.
|
479
|
+
def dirty?
|
480
|
+
!@modified.empty? || !@v4_requests.empty?
|
481
|
+
end
|
482
|
+
|
483
|
+
# List feed URL of the worksheet.
|
484
|
+
#
|
485
|
+
# DEPRECATED: This method is deprecated, and now requires additional
|
486
|
+
# network fetch.
|
487
|
+
def list_feed_url
|
488
|
+
@worksheet_feed_entry.css(
|
489
|
+
"link[rel='http://schemas.google.com/spreadsheets/2006#listfeed']"
|
490
|
+
)[0]['href']
|
491
|
+
end
|
492
|
+
|
493
|
+
# Provides access to cells using column names, assuming the first row
|
494
|
+
# contains column
|
495
|
+
# names. Returned object is GoogleDrive::List which you can use mostly as
|
496
|
+
# Array of Hash.
|
497
|
+
#
|
498
|
+
# e.g. Assuming the first row is ["x", "y"]:
|
499
|
+
# worksheet.list[0]["x"] #=> "1" # i.e. worksheet[2, 1]
|
500
|
+
# worksheet.list[0]["y"] #=> "2" # i.e. worksheet[2, 2]
|
501
|
+
# worksheet.list[1]["x"] = "3" # i.e. worksheet[3, 1] = "3"
|
502
|
+
# worksheet.list[1]["y"] = "4" # i.e. worksheet[3, 2] = "4"
|
503
|
+
# worksheet.list.push({"x" => "5", "y" => "6"})
|
504
|
+
#
|
505
|
+
# Note that update is not sent to the server until you call save().
|
506
|
+
def list
|
507
|
+
@list ||= List.new(self)
|
508
|
+
end
|
509
|
+
|
510
|
+
# Returns a [row, col] pair for a cell name string.
|
511
|
+
# e.g.
|
512
|
+
# worksheet.cell_name_to_row_col("C2") #=> [2, 3]
|
513
|
+
def cell_name_to_row_col(cell_name)
|
514
|
+
unless cell_name.is_a?(String)
|
515
|
+
raise(
|
516
|
+
ArgumentError, format('Cell name must be a string: %p', cell_name)
|
517
|
+
)
|
518
|
+
end
|
519
|
+
unless cell_name.upcase =~ /^([A-Z]+)(\d+)$/
|
520
|
+
raise(
|
521
|
+
ArgumentError,
|
522
|
+
format(
|
523
|
+
'Cell name must be only letters followed by digits with no ' \
|
524
|
+
'spaces in between: %p',
|
525
|
+
cell_name
|
526
|
+
)
|
527
|
+
)
|
528
|
+
end
|
529
|
+
col = 0
|
530
|
+
Regexp.last_match(1).each_byte do |b|
|
531
|
+
# 0x41: "A"
|
532
|
+
col = col * 26 + (b - 0x41 + 1)
|
533
|
+
end
|
534
|
+
row = Regexp.last_match(2).to_i
|
535
|
+
[row, col]
|
536
|
+
end
|
537
|
+
|
538
|
+
def inspect
|
539
|
+
fields = { spreadsheet_id: spreadsheet.id, gid: gid }
|
540
|
+
fields[:title] = @title if @title
|
541
|
+
format(
|
542
|
+
"\#<%p %s>",
|
543
|
+
self.class,
|
544
|
+
fields.map { |k, v| format('%s=%p', k, v) }.join(', ')
|
545
|
+
)
|
546
|
+
end
|
547
|
+
|
548
|
+
# Merges a range of cells together. "MERGE_COLUMNS" is another option for merge_type
|
549
|
+
def merge_cells(top_row, left_col, num_rows, num_cols, merge_type: 'MERGE_ALL')
|
550
|
+
range = v4_range_object(top_row, left_col, num_rows, num_cols)
|
551
|
+
add_request({
|
552
|
+
merge_cells:
|
553
|
+
Google::Apis::SheetsV4::MergeCellsRequest.new(
|
554
|
+
range: range, merge_type: merge_type),
|
555
|
+
})
|
556
|
+
end
|
557
|
+
|
558
|
+
# Changes the formatting of a range of cells to match the given number format.
|
559
|
+
# For example to change A1 to a percentage with 1 decimal point:
|
560
|
+
# worksheet.set_number_format(1, 1, 1, 1, "##.#%")
|
561
|
+
# Google API reference: https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets#numberformat
|
562
|
+
def set_number_format(top_row, left_col, num_rows, num_cols, pattern, type: "NUMBER")
|
563
|
+
number_format = Google::Apis::SheetsV4::NumberFormat.new(type: type, pattern: pattern)
|
564
|
+
format = Google::Apis::SheetsV4::CellFormat.new(number_format: number_format)
|
565
|
+
fields = 'userEnteredFormat(numberFormat)'
|
566
|
+
format_cells(top_row, left_col, num_rows, num_cols, format, fields)
|
567
|
+
end
|
568
|
+
|
569
|
+
# Changes text alignment of a range of cells.
|
570
|
+
# Horizontal alignment can be "LEFT", "CENTER", or "RIGHT".
|
571
|
+
# Vertical alignment can be "TOP", "MIDDLE", or "BOTTOM".
|
572
|
+
# Google API reference: https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets#HorizontalAlign
|
573
|
+
def set_text_alignment(
|
574
|
+
top_row, left_col, num_rows, num_cols,
|
575
|
+
horizontal: nil, vertical: nil)
|
576
|
+
return if horizontal.nil? && vertical.nil?
|
577
|
+
|
578
|
+
format = Google::Apis::SheetsV4::CellFormat.new(
|
579
|
+
horizontal_alignment: horizontal, vertical_alignment: vertical)
|
580
|
+
subfields =
|
581
|
+
(horizontal.nil? ? [] : ['horizontalAlignment']) +
|
582
|
+
(vertical.nil? ? [] : ['verticalAlignment'])
|
583
|
+
|
584
|
+
fields = 'userEnteredFormat(%s)' % subfields.join(',')
|
585
|
+
format_cells(top_row, left_col, num_rows, num_cols, format, fields)
|
586
|
+
end
|
587
|
+
|
588
|
+
# Changes the background color on a range of cells. e.g.:
|
589
|
+
# worksheet.set_background_color(1, 1, 1, 1, GoogleDrive::Worksheet::Colors::DARK_YELLOW_1)
|
590
|
+
#
|
591
|
+
# background_color is an instance of Google::Apis::SheetsV4::Color.
|
592
|
+
def set_background_color(top_row, left_col, num_rows, num_cols, background_color)
|
593
|
+
format = Google::Apis::SheetsV4::CellFormat.new(background_color: background_color)
|
594
|
+
fields = 'userEnteredFormat(backgroundColor)'
|
595
|
+
format_cells(top_row, left_col, num_rows, num_cols, format, fields)
|
596
|
+
end
|
597
|
+
|
598
|
+
# Change the text formatting on a range of cells. e.g., To set cell
|
599
|
+
# A1 to have red text that is bold and italic:
|
600
|
+
# worksheet.set_text_format(
|
601
|
+
# 1, 1, 1, 1,
|
602
|
+
# bold: true,
|
603
|
+
# italic: true,
|
604
|
+
# foreground_color: GoogleDrive::Worksheet::Colors::RED_BERRY)
|
605
|
+
#
|
606
|
+
# foreground_color is an instance of Google::Apis::SheetsV4::Color.
|
607
|
+
# Google API reference:
|
608
|
+
# https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets#textformat
|
609
|
+
def set_text_format(top_row, left_col, num_rows, num_cols, bold: false,
|
610
|
+
italic: false, strikethrough: false, font_size: nil,
|
611
|
+
font_family: nil, foreground_color: nil)
|
612
|
+
text_format = Google::Apis::SheetsV4::TextFormat.new(
|
613
|
+
bold: bold,
|
614
|
+
italic: italic,
|
615
|
+
strikethrough: strikethrough,
|
616
|
+
font_size: font_size,
|
617
|
+
font_family: font_family,
|
618
|
+
foreground_color: foreground_color
|
619
|
+
)
|
620
|
+
|
621
|
+
format = Google::Apis::SheetsV4::CellFormat.new(text_format: text_format)
|
622
|
+
fields = 'userEnteredFormat(textFormat)'
|
623
|
+
format_cells(top_row, left_col, num_rows, num_cols, format, fields)
|
624
|
+
end
|
625
|
+
|
626
|
+
# Update the border styles for a range of cells.
|
627
|
+
# borders is a Hash of Google::Apis::SheetsV4::Border keyed with the
|
628
|
+
# following symbols: :top, :bottom, :left, :right, :innerHorizontal, :innerVertical
|
629
|
+
# e.g., To set a black double-line on the bottom of A1:
|
630
|
+
# update_borders(
|
631
|
+
# 1, 1, 1, 1,
|
632
|
+
# {bottom: Google::Apis::SheetsV4::Border.new(
|
633
|
+
# style: "DOUBLE", color: GoogleDrive::Worksheet::Colors::BLACK)})
|
634
|
+
def update_borders(top_row, left_col, num_rows, num_cols, borders)
|
635
|
+
request = Google::Apis::SheetsV4::UpdateBordersRequest.new(borders)
|
636
|
+
request.range = v4_range_object(top_row, left_col, num_rows, num_cols)
|
637
|
+
add_request({update_borders: request})
|
638
|
+
end
|
639
|
+
|
640
|
+
# Add an instance of Google::Apis::SheetsV4::Request (or its Hash
|
641
|
+
# equivalent) which will be applied on the next call to the save method.
|
642
|
+
def add_request(request)
|
643
|
+
@v4_requests.push(request)
|
644
|
+
end
|
645
|
+
|
646
|
+
# @api private
|
647
|
+
def worksheet_feed_id
|
648
|
+
gid_int = sheet_id
|
649
|
+
xor_val = gid_int > 31578 ? 474 : 31578
|
650
|
+
letter = gid_int > 31578 ? 'o' : ''
|
651
|
+
letter + (gid_int ^ xor_val).to_s(36)
|
652
|
+
end
|
653
|
+
|
654
|
+
private
|
655
|
+
|
656
|
+
def format_cells(top_row, left_col, num_rows, num_cols, format, fields)
|
657
|
+
add_request({
|
658
|
+
repeat_cell:
|
659
|
+
Google::Apis::SheetsV4::RepeatCellRequest.new(
|
660
|
+
range: v4_range_object(top_row, left_col, num_rows, num_cols),
|
661
|
+
cell: Google::Apis::SheetsV4::CellData.new(user_entered_format: format),
|
662
|
+
fields: fields
|
663
|
+
),
|
664
|
+
})
|
665
|
+
end
|
666
|
+
|
667
|
+
def set_properties(properties)
|
668
|
+
@properties = properties
|
669
|
+
@title = @remote_title = properties.title
|
670
|
+
@index = properties.index
|
671
|
+
if properties.grid_properties.nil?
|
672
|
+
@max_rows = @max_cols = 0
|
673
|
+
else
|
674
|
+
@max_rows = properties.grid_properties.row_count
|
675
|
+
@max_cols = properties.grid_properties.column_count
|
676
|
+
end
|
677
|
+
@meta_modified = false
|
678
|
+
end
|
679
|
+
|
680
|
+
def reload_cells
|
681
|
+
response =
|
682
|
+
@session.sheets_service.get_spreadsheet(
|
683
|
+
spreadsheet.id,
|
684
|
+
ranges: "'%s'" % @remote_title,
|
685
|
+
fields: 'sheets.data.rowData.values(formattedValue,userEnteredValue,effectiveValue)'
|
686
|
+
)
|
687
|
+
update_cells_from_api_sheet(response.sheets[0])
|
688
|
+
end
|
689
|
+
|
690
|
+
def update_cells_from_api_sheet(api_sheet)
|
691
|
+
rows_data = api_sheet.data[0].row_data || []
|
692
|
+
|
693
|
+
@num_rows = rows_data.size
|
694
|
+
@num_cols = 0
|
695
|
+
@cells = {}
|
696
|
+
@input_values = {}
|
697
|
+
@numeric_values = {}
|
698
|
+
|
699
|
+
rows_data.each_with_index do |row_data, r|
|
700
|
+
next if !row_data.values
|
701
|
+
@num_cols = row_data.values.size if row_data.values.size > @num_cols
|
702
|
+
row_data.values.each_with_index do |cell_data, c|
|
703
|
+
k = [r + 1, c + 1]
|
704
|
+
@cells[k] = cell_data.formatted_value || ''
|
705
|
+
@input_values[k] = extended_value_to_str(cell_data.user_entered_value)
|
706
|
+
@numeric_values[k] =
|
707
|
+
cell_data.effective_value && cell_data.effective_value.number_value ?
|
708
|
+
cell_data.effective_value.number_value.to_f : nil
|
709
|
+
end
|
710
|
+
end
|
711
|
+
|
712
|
+
@modified.clear
|
713
|
+
end
|
714
|
+
|
715
|
+
def parse_cell_args(args)
|
716
|
+
if args.size == 1 && args[0].is_a?(String)
|
717
|
+
cell_name_to_row_col(args[0])
|
718
|
+
elsif args.size == 2 && args[0].is_a?(Integer) && args[1].is_a?(Integer)
|
719
|
+
if args[0] >= 1 && args[1] >= 1
|
720
|
+
args
|
721
|
+
else
|
722
|
+
raise(
|
723
|
+
ArgumentError,
|
724
|
+
format(
|
725
|
+
'Row/col must be >= 1 (1-origin), but are %d/%d',
|
726
|
+
args[0], args[1]
|
727
|
+
)
|
728
|
+
)
|
729
|
+
end
|
730
|
+
else
|
731
|
+
raise(
|
732
|
+
ArgumentError,
|
733
|
+
format(
|
734
|
+
"Arguments must be either one String or two Integer's, but are %p",
|
735
|
+
args
|
736
|
+
)
|
737
|
+
)
|
738
|
+
end
|
739
|
+
end
|
740
|
+
|
741
|
+
def validate_cell_value(value)
|
742
|
+
if value =~ XML_INVALID_CHAR_REGEXP
|
743
|
+
raise(
|
744
|
+
ArgumentError,
|
745
|
+
format('Contains invalid character %p for XML 1.0: %p', $&, value)
|
746
|
+
)
|
747
|
+
end
|
748
|
+
end
|
749
|
+
|
750
|
+
def v4_range_object(top_row, left_col, num_rows, num_cols)
|
751
|
+
Google::Apis::SheetsV4::GridRange.new(
|
752
|
+
sheet_id: sheet_id,
|
753
|
+
start_row_index: top_row - 1,
|
754
|
+
start_column_index: left_col - 1,
|
755
|
+
end_row_index: top_row + num_rows - 1,
|
756
|
+
end_column_index: left_col + num_cols - 1
|
757
|
+
)
|
758
|
+
end
|
759
|
+
|
760
|
+
def extended_value_to_str(extended_value)
|
761
|
+
return '' if !extended_value
|
762
|
+
value =
|
763
|
+
extended_value.number_value ||
|
764
|
+
extended_value.string_value ||
|
765
|
+
extended_value.bool_value ||
|
766
|
+
extended_value.formula_value
|
767
|
+
value.to_s
|
768
|
+
end
|
769
|
+
end
|
770
|
+
end
|