google_drive 1.0.6 → 2.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 +4 -4
- data/README.rdoc +3 -7
- data/doc_src/google_drive/acl.rb +13 -17
- data/lib/google_drive.rb +130 -160
- data/lib/google_drive/acl.rb +77 -93
- data/lib/google_drive/acl_entry.rb +149 -105
- data/lib/google_drive/api_client_fetcher.rb +22 -41
- data/lib/google_drive/authentication_error.rb +4 -8
- data/lib/google_drive/collection.rb +127 -149
- data/lib/google_drive/config.rb +23 -25
- data/lib/google_drive/error.rb +3 -3
- data/lib/google_drive/file.rb +215 -273
- data/lib/google_drive/list.rb +108 -113
- data/lib/google_drive/list_row.rb +65 -70
- data/lib/google_drive/response_code_error.rb +11 -16
- data/lib/google_drive/session.rb +412 -444
- data/lib/google_drive/spreadsheet.rb +62 -67
- data/lib/google_drive/util.rb +200 -160
- data/lib/google_drive/worksheet.rb +453 -469
- metadata +60 -22
@@ -1,377 +1,366 @@
|
|
1
1
|
# Author: Hiroshi Ichikawa <http://gimite.net/>
|
2
2
|
# The license of this source is "New BSD Licence"
|
3
3
|
|
4
|
-
require
|
5
|
-
require
|
6
|
-
require
|
7
|
-
|
8
|
-
require "google_drive/util"
|
9
|
-
require "google_drive/error"
|
10
|
-
require "google_drive/list"
|
4
|
+
require 'cgi'
|
5
|
+
require 'set'
|
6
|
+
require 'uri'
|
11
7
|
|
8
|
+
require 'google_drive/util'
|
9
|
+
require 'google_drive/error'
|
10
|
+
require 'google_drive/list'
|
12
11
|
|
13
12
|
module GoogleDrive
|
13
|
+
# A worksheet (i.e. a tab) in a spreadsheet.
|
14
|
+
# Use GoogleDrive::Spreadsheet#worksheets to get GoogleDrive::Worksheet object.
|
15
|
+
class Worksheet
|
16
|
+
include(Util)
|
17
|
+
|
18
|
+
def initialize(session, spreadsheet, worksheet_feed_entry) #:nodoc:
|
19
|
+
@session = session
|
20
|
+
@spreadsheet = spreadsheet
|
21
|
+
set_worksheet_feed_entry(worksheet_feed_entry)
|
22
|
+
|
23
|
+
@cells = nil
|
24
|
+
@input_values = nil
|
25
|
+
@numeric_values = nil
|
26
|
+
@modified = Set.new
|
27
|
+
@list = nil
|
28
|
+
end
|
14
29
|
|
15
|
-
#
|
16
|
-
|
17
|
-
class Worksheet
|
18
|
-
|
19
|
-
include(Util)
|
20
|
-
|
21
|
-
def initialize(session, spreadsheet, worksheet_feed_entry) #:nodoc:
|
22
|
-
|
23
|
-
@session = session
|
24
|
-
@spreadsheet = spreadsheet
|
25
|
-
set_worksheet_feed_entry(worksheet_feed_entry)
|
26
|
-
|
27
|
-
@cells = nil
|
28
|
-
@input_values = nil
|
29
|
-
@numeric_values = nil
|
30
|
-
@modified = Set.new()
|
31
|
-
@list = nil
|
32
|
-
|
33
|
-
end
|
34
|
-
|
35
|
-
# Nokogiri::XML::Element object of the <entry> element in a worksheets feed.
|
36
|
-
attr_reader(:worksheet_feed_entry)
|
37
|
-
|
38
|
-
# Title of the worksheet (shown as tab label in Web interface).
|
39
|
-
attr_reader(:title)
|
40
|
-
|
41
|
-
# Time object which represents the time the worksheet was last updated.
|
42
|
-
attr_reader(:updated)
|
30
|
+
# Nokogiri::XML::Element object of the <entry> element in a worksheets feed.
|
31
|
+
attr_reader(:worksheet_feed_entry)
|
43
32
|
|
44
|
-
|
45
|
-
|
46
|
-
return @worksheet_feed_entry.css(
|
47
|
-
"link[rel='http://schemas.google.com/spreadsheets/2006#cellsfeed']")[0]["href"]
|
48
|
-
end
|
33
|
+
# Title of the worksheet (shown as tab label in Web interface).
|
34
|
+
attr_reader(:title)
|
49
35
|
|
50
|
-
|
51
|
-
|
52
|
-
return @worksheet_feed_entry.css("link[rel='self']")[0]["href"]
|
53
|
-
end
|
36
|
+
# Time object which represents the time the worksheet was last updated.
|
37
|
+
attr_reader(:updated)
|
54
38
|
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
39
|
+
# URL of cell-based feed of the worksheet.
|
40
|
+
def cells_feed_url
|
41
|
+
@worksheet_feed_entry.css(
|
42
|
+
"link[rel='http://schemas.google.com/spreadsheets/2006#cellsfeed']")[0]['href']
|
43
|
+
end
|
60
44
|
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
end
|
45
|
+
# URL of worksheet feed URL of the worksheet.
|
46
|
+
def worksheet_feed_url
|
47
|
+
@worksheet_feed_entry.css("link[rel='self']")[0]['href']
|
48
|
+
end
|
66
49
|
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
50
|
+
# URL to export the worksheet as CSV.
|
51
|
+
def csv_export_url
|
52
|
+
@worksheet_feed_entry.css(
|
53
|
+
"link[rel='http://schemas.google.com/spreadsheets/2006#exportcsv']")[0]['href']
|
54
|
+
end
|
72
55
|
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
end
|
56
|
+
# Exports the worksheet as String in CSV format.
|
57
|
+
def export_as_string
|
58
|
+
@session.request(:get, csv_export_url, response_type: :raw)
|
59
|
+
end
|
78
60
|
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
61
|
+
# Exports the worksheet to +path+ in CSV format.
|
62
|
+
def export_as_file(path)
|
63
|
+
data = export_as_string
|
64
|
+
open(path, 'wb') { |f| f.write(data) }
|
65
|
+
end
|
83
66
|
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
"Worksheet feed URL is in unknown format: #{self.worksheet_feed_url}")
|
90
|
-
end
|
91
|
-
@spreadsheet = @session.file_by_id($1)
|
92
|
-
end
|
93
|
-
return @spreadsheet
|
94
|
-
end
|
67
|
+
# gid of the worksheet.
|
68
|
+
def gid
|
69
|
+
# A bit tricky but couldn't find a better way.
|
70
|
+
CGI.parse(URI.parse(csv_export_url).query)['gid'].last
|
71
|
+
end
|
95
72
|
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
# worksheet[2, 1] #=> "hoge"
|
101
|
-
# worksheet["A2"] #=> "hoge"
|
102
|
-
def [](*args)
|
103
|
-
(row, col) = parse_cell_args(args)
|
104
|
-
return self.cells[[row, col]] || ""
|
105
|
-
end
|
73
|
+
# URL to view/edit the worksheet in a Web browser.
|
74
|
+
def human_url
|
75
|
+
"%s\#gid=%s" % [spreadsheet.human_url, gid]
|
76
|
+
end
|
106
77
|
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
# worksheet[2, 1] = "hoge"
|
114
|
-
# worksheet["A2"] = "hoge"
|
115
|
-
# worksheet[1, 3] = "=A1+B1"
|
116
|
-
def []=(*args)
|
117
|
-
(row, col) = parse_cell_args(args[0...-1])
|
118
|
-
value = args[-1].to_s()
|
119
|
-
validate_cell_value(value)
|
120
|
-
reload_cells() if !@cells
|
121
|
-
@cells[[row, col]] = value
|
122
|
-
@input_values[[row, col]] = value
|
123
|
-
@numeric_values[[row, col]] = nil
|
124
|
-
@modified.add([row, col])
|
125
|
-
self.max_rows = row if row > @max_rows
|
126
|
-
self.max_cols = col if col > @max_cols
|
127
|
-
if value.empty?
|
128
|
-
@num_rows = nil
|
129
|
-
@num_cols = nil
|
130
|
-
else
|
131
|
-
@num_rows = row if row > num_rows
|
132
|
-
@num_cols = col if col > num_cols
|
133
|
-
end
|
78
|
+
# GoogleDrive::Spreadsheet which this worksheet belongs to.
|
79
|
+
def spreadsheet
|
80
|
+
unless @spreadsheet
|
81
|
+
unless worksheet_feed_url =~ %r{https?://spreadsheets\.google\.com/feeds/worksheets/(.*)/(.*)$}
|
82
|
+
fail(GoogleDrive::Error,
|
83
|
+
"Worksheet feed URL is in unknown format: #{worksheet_feed_url}")
|
134
84
|
end
|
85
|
+
@spreadsheet = @session.file_by_id(Regexp.last_match(1))
|
86
|
+
end
|
87
|
+
@spreadsheet
|
88
|
+
end
|
135
89
|
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
end
|
147
|
-
end
|
90
|
+
# Returns content of the cell as String. Arguments must be either
|
91
|
+
# (row number, column number) or cell name. Top-left cell is [1, 1].
|
92
|
+
#
|
93
|
+
# e.g.
|
94
|
+
# worksheet[2, 1] #=> "hoge"
|
95
|
+
# worksheet["A2"] #=> "hoge"
|
96
|
+
def [](*args)
|
97
|
+
(row, col) = parse_cell_args(args)
|
98
|
+
cells[[row, col]] || ''
|
99
|
+
end
|
148
100
|
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
101
|
+
# Updates content of the cell.
|
102
|
+
# Arguments in the bracket must be either (row number, column number) or cell name.
|
103
|
+
# Note that update is not sent to the server until you call save().
|
104
|
+
# Top-left cell is [1, 1].
|
105
|
+
#
|
106
|
+
# e.g.
|
107
|
+
# worksheet[2, 1] = "hoge"
|
108
|
+
# worksheet["A2"] = "hoge"
|
109
|
+
# worksheet[1, 3] = "=A1+B1"
|
110
|
+
def []=(*args)
|
111
|
+
(row, col) = parse_cell_args(args[0...-1])
|
112
|
+
value = args[-1].to_s
|
113
|
+
validate_cell_value(value)
|
114
|
+
reload_cells unless @cells
|
115
|
+
@cells[[row, col]] = value
|
116
|
+
@input_values[[row, col]] = value
|
117
|
+
@numeric_values[[row, col]] = nil
|
118
|
+
@modified.add([row, col])
|
119
|
+
self.max_rows = row if row > @max_rows
|
120
|
+
self.max_cols = col if col > @max_cols
|
121
|
+
if value.empty?
|
122
|
+
@num_rows = nil
|
123
|
+
@num_cols = nil
|
124
|
+
else
|
125
|
+
@num_rows = row if row > num_rows
|
126
|
+
@num_cols = col if col > num_cols
|
127
|
+
end
|
128
|
+
end
|
160
129
|
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
# https://developers.google.com/google-apps/spreadsheets/#working_with_cell-based_feeds
|
174
|
-
def numeric_value(*args)
|
175
|
-
(row, col) = parse_cell_args(args)
|
176
|
-
reload_cells() if !@cells
|
177
|
-
return @numeric_values[[row, col]]
|
178
|
-
end
|
179
|
-
|
180
|
-
# Row number of the bottom-most non-empty row.
|
181
|
-
def num_rows
|
182
|
-
reload_cells() if !@cells
|
183
|
-
# Memoizes it because this can be bottle-neck.
|
184
|
-
# https://github.com/gimite/google-drive-ruby/pull/49
|
185
|
-
return @num_rows ||= @input_values.select(){ |(r, c), v| !v.empty? }.map(){ |(r, c), v| r }.max || 0
|
186
|
-
end
|
130
|
+
# Updates cells in a rectangle area by a two-dimensional Array.
|
131
|
+
# +top_row+ and +left_col+ specifies the top-left corner of the area.
|
132
|
+
#
|
133
|
+
# e.g.
|
134
|
+
# worksheet.update_cells(2, 3, [["1", "2"], ["3", "4"]])
|
135
|
+
def update_cells(top_row, left_col, darray)
|
136
|
+
darray.each_with_index do |array, y|
|
137
|
+
array.each_with_index do |value, x|
|
138
|
+
self[top_row + y, left_col + x] = value
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
187
142
|
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
143
|
+
# Returns the value or the formula of the cell. Arguments must be either
|
144
|
+
# (row number, column number) or cell name. Top-left cell is [1, 1].
|
145
|
+
#
|
146
|
+
# If user input "=A1+B1" to cell [1, 3]:
|
147
|
+
# worksheet[1, 3] #=> "3" for example
|
148
|
+
# worksheet.input_value(1, 3) #=> "=RC[-2]+RC[-1]"
|
149
|
+
def input_value(*args)
|
150
|
+
(row, col) = parse_cell_args(args)
|
151
|
+
reload_cells unless @cells
|
152
|
+
@input_values[[row, col]] || ''
|
153
|
+
end
|
195
154
|
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
155
|
+
# Returns the numeric value of the cell. Arguments must be either
|
156
|
+
# (row number, column number) or cell name. Top-left cell is [1, 1].
|
157
|
+
#
|
158
|
+
# e.g.
|
159
|
+
# worksheet[1, 3] #=> "3,0" # it depends on locale, currency...
|
160
|
+
# worksheet.numeric_value(1, 3) #=> 3.0
|
161
|
+
#
|
162
|
+
# Returns nil if the cell is empty or contains non-number.
|
163
|
+
#
|
164
|
+
# If you modify the cell, its numeric_value is nil until you call save() and reload().
|
165
|
+
#
|
166
|
+
# For details, see:
|
167
|
+
# https://developers.google.com/google-apps/spreadsheets/#working_with_cell-based_feeds
|
168
|
+
def numeric_value(*args)
|
169
|
+
(row, col) = parse_cell_args(args)
|
170
|
+
reload_cells unless @cells
|
171
|
+
@numeric_values[[row, col]]
|
172
|
+
end
|
201
173
|
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
174
|
+
# Row number of the bottom-most non-empty row.
|
175
|
+
def num_rows
|
176
|
+
reload_cells unless @cells
|
177
|
+
# Memoizes it because this can be bottle-neck.
|
178
|
+
# https://github.com/gimite/google-drive-ruby/pull/49
|
179
|
+
@num_rows ||= @input_values.select { |(_r, _c), v| !v.empty? }.map { |(r, _c), _v| r }.max || 0
|
180
|
+
end
|
209
181
|
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
182
|
+
# Column number of the right-most non-empty column.
|
183
|
+
def num_cols
|
184
|
+
reload_cells unless @cells
|
185
|
+
# Memoizes it because this can be bottle-neck.
|
186
|
+
# https://github.com/gimite/google-drive-ruby/pull/49
|
187
|
+
@num_cols ||= @input_values.select { |(_r, _c), v| !v.empty? }.map { |(_r, c), _v| c }.max || 0
|
188
|
+
end
|
215
189
|
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
@meta_modified = true
|
222
|
-
end
|
190
|
+
# Number of rows including empty rows.
|
191
|
+
def max_rows
|
192
|
+
reload_cells unless @cells
|
193
|
+
@max_rows
|
194
|
+
end
|
223
195
|
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
196
|
+
# Updates number of rows.
|
197
|
+
# Note that update is not sent to the server until you call save().
|
198
|
+
def max_rows=(rows)
|
199
|
+
reload_cells unless @cells
|
200
|
+
@max_rows = rows
|
201
|
+
@meta_modified = true
|
202
|
+
end
|
230
203
|
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
204
|
+
# Number of columns including empty columns.
|
205
|
+
def max_cols
|
206
|
+
reload_cells unless @cells
|
207
|
+
@max_cols
|
208
|
+
end
|
235
209
|
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
(1..nc).map(){ |col| self[row, col] }.freeze()
|
244
|
-
end
|
245
|
-
return result.freeze()
|
246
|
-
end
|
210
|
+
# Updates number of columns.
|
211
|
+
# Note that update is not sent to the server until you call save().
|
212
|
+
def max_cols=(cols)
|
213
|
+
reload_cells unless @cells
|
214
|
+
@max_cols = cols
|
215
|
+
@meta_modified = true
|
216
|
+
end
|
247
217
|
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
# worksheet.insert_rows(3, [["a, "b"], ["c, "d"]])
|
255
|
-
#
|
256
|
-
# Note that this method is implemented by shifting all cells below the row.
|
257
|
-
# Its behavior is different from inserting rows on the web interface if the
|
258
|
-
# worksheet contains inter-cell reference.
|
259
|
-
def insert_rows(row_num, rows)
|
260
|
-
|
261
|
-
if rows.is_a?(Integer)
|
262
|
-
rows = Array.new(rows, [])
|
263
|
-
end
|
218
|
+
# Updates title of the worksheet.
|
219
|
+
# Note that update is not sent to the server until you call save().
|
220
|
+
def title=(title)
|
221
|
+
@title = title
|
222
|
+
@meta_modified = true
|
223
|
+
end
|
264
224
|
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
for c in 1..self.num_cols
|
270
|
-
self[r + rows.size, c] = self[r, c]
|
271
|
-
end
|
272
|
-
r -= 1
|
273
|
-
end
|
225
|
+
def cells #:nodoc:
|
226
|
+
reload_cells unless @cells
|
227
|
+
@cells
|
228
|
+
end
|
274
229
|
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
230
|
+
# An array of spreadsheet rows. Each row contains an array of
|
231
|
+
# columns. Note that resulting array is 0-origin so:
|
232
|
+
#
|
233
|
+
# worksheet.rows[0][0] == worksheet[1, 1]
|
234
|
+
def rows(skip = 0)
|
235
|
+
nc = num_cols
|
236
|
+
result = ((1 + skip)..num_rows).map do |row|
|
237
|
+
(1..nc).map { |col| self[row, col] }.freeze
|
238
|
+
end
|
239
|
+
result.freeze
|
240
|
+
end
|
282
241
|
|
283
|
-
|
242
|
+
# Inserts rows.
|
243
|
+
#
|
244
|
+
# e.g.
|
245
|
+
# # Inserts 2 empty rows before row 3.
|
246
|
+
# worksheet.insert_rows(3, 2)
|
247
|
+
# # Inserts 2 rows with values before row 3.
|
248
|
+
# worksheet.insert_rows(3, [["a, "b"], ["c, "d"]])
|
249
|
+
#
|
250
|
+
# Note that this method is implemented by shifting all cells below the row.
|
251
|
+
# Its behavior is different from inserting rows on the web interface if the
|
252
|
+
# worksheet contains inter-cell reference.
|
253
|
+
def insert_rows(row_num, rows)
|
254
|
+
rows = Array.new(rows, []) if rows.is_a?(Integer)
|
255
|
+
|
256
|
+
# Shifts all cells below the row.
|
257
|
+
self.max_rows += rows.size
|
258
|
+
r = num_rows
|
259
|
+
while r >= row_num
|
260
|
+
for c in 1..num_cols
|
261
|
+
self[r + rows.size, c] = self[r, c]
|
262
|
+
end
|
263
|
+
r -= 1
|
264
|
+
end
|
265
|
+
|
266
|
+
# Fills in the inserted rows.
|
267
|
+
num_cols = self.num_cols
|
268
|
+
rows.each_with_index do |row, r|
|
269
|
+
for c in 0...[row.size, num_cols].max
|
270
|
+
self[row_num + r, 1 + c] = row[c] || ''
|
271
|
+
end
|
272
|
+
end
|
273
|
+
end
|
284
274
|
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
275
|
+
# Deletes rows.
|
276
|
+
#
|
277
|
+
# e.g.
|
278
|
+
# # Deletes 2 rows starting from row 3 (i.e., deletes row 3 and 4).
|
279
|
+
# worksheet.delete_rows(3, 2)
|
280
|
+
#
|
281
|
+
# Note that this method is implemented by shifting all cells below the row.
|
282
|
+
# Its behavior is different from deleting rows on the web interface if the
|
283
|
+
# worksheet contains inter-cell reference.
|
284
|
+
def delete_rows(row_num, rows)
|
285
|
+
if row_num + rows - 1 > self.max_rows
|
286
|
+
fail(ArgumentError, 'The row number is out of range')
|
287
|
+
end
|
288
|
+
for r in row_num..(self.max_rows - rows)
|
289
|
+
for c in 1..num_cols
|
290
|
+
self[r, c] = self[r + rows, c]
|
291
|
+
end
|
292
|
+
end
|
293
|
+
self.max_rows -= rows
|
294
|
+
end
|
305
295
|
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
296
|
+
# Reloads content of the worksheets from the server.
|
297
|
+
# Note that changes you made by []= etc. is discarded if you haven't called save().
|
298
|
+
def reload
|
299
|
+
set_worksheet_feed_entry(@session.request(:get, worksheet_feed_url).root)
|
300
|
+
reload_cells
|
301
|
+
true
|
302
|
+
end
|
313
303
|
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
sent = false
|
304
|
+
# Saves your changes made by []=, etc. to the server.
|
305
|
+
def save
|
306
|
+
sent = false
|
318
307
|
|
319
|
-
|
308
|
+
if @meta_modified
|
320
309
|
|
321
|
-
|
322
|
-
|
310
|
+
edit_url = @worksheet_feed_entry.css("link[rel='edit']")[0]['href']
|
311
|
+
xml = <<-"EOS"
|
323
312
|
<entry xmlns='http://www.w3.org/2005/Atom'
|
324
313
|
xmlns:gs='http://schemas.google.com/spreadsheets/2006'>
|
325
|
-
<title>#{h(
|
314
|
+
<title>#{h(title)}</title>
|
326
315
|
<gs:rowCount>#{h(self.max_rows)}</gs:rowCount>
|
327
|
-
<gs:colCount>#{h(
|
316
|
+
<gs:colCount>#{h(max_cols)}</gs:colCount>
|
328
317
|
</entry>
|
329
318
|
EOS
|
330
319
|
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
320
|
+
result = @session.request(
|
321
|
+
:put, edit_url, data: xml,
|
322
|
+
header: { 'Content-Type' => 'application/atom+xml;charset=utf-8', 'If-Match' => '*' })
|
323
|
+
set_worksheet_feed_entry(result.root)
|
335
324
|
|
336
|
-
|
325
|
+
sent = true
|
337
326
|
|
338
|
-
|
327
|
+
end
|
328
|
+
|
329
|
+
unless @modified.empty?
|
330
|
+
|
331
|
+
# Gets id and edit URL for each cell.
|
332
|
+
# Note that return-empty=true is required to get those info for empty cells.
|
333
|
+
cell_entries = {}
|
334
|
+
rows = @modified.map { |r, _c| r }
|
335
|
+
cols = @modified.map { |_r, c| c }
|
336
|
+
url = concat_url(cells_feed_url,
|
337
|
+
"?return-empty=true&min-row=#{rows.min}&max-row=#{rows.max}" \
|
338
|
+
"&min-col=#{cols.min}&max-col=#{cols.max}")
|
339
|
+
doc = @session.request(:get, url)
|
339
340
|
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
url = concat_url(self.cells_feed_url,
|
348
|
-
"?return-empty=true&min-row=#{rows.min}&max-row=#{rows.max}" +
|
349
|
-
"&min-col=#{cols.min}&max-col=#{cols.max}")
|
350
|
-
doc = @session.request(:get, url)
|
351
|
-
|
352
|
-
for entry in doc.css("entry")
|
353
|
-
row = entry.css("gs|cell")[0]["row"].to_i()
|
354
|
-
col = entry.css("gs|cell")[0]["col"].to_i()
|
355
|
-
cell_entries[[row, col]] = entry
|
356
|
-
end
|
357
|
-
|
358
|
-
xml = <<-EOS
|
341
|
+
doc.css('entry').each do |entry|
|
342
|
+
row = entry.css('gs|cell')[0]['row'].to_i
|
343
|
+
col = entry.css('gs|cell')[0]['col'].to_i
|
344
|
+
cell_entries[[row, col]] = entry
|
345
|
+
end
|
346
|
+
|
347
|
+
xml = <<-EOS
|
359
348
|
<feed xmlns="http://www.w3.org/2005/Atom"
|
360
349
|
xmlns:batch="http://schemas.google.com/gdata/batch"
|
361
350
|
xmlns:gs="http://schemas.google.com/spreadsheets/2006">
|
362
|
-
<id>#{h(
|
351
|
+
<id>#{h(cells_feed_url)}</id>
|
363
352
|
EOS
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
353
|
+
@modified.each do |row, col|
|
354
|
+
value = @cells[[row, col]]
|
355
|
+
entry = cell_entries[[row, col]]
|
356
|
+
id = entry.css('id').text
|
357
|
+
edit_link = entry.css("link[rel='edit']")[0]
|
358
|
+
unless edit_link
|
359
|
+
fail(GoogleDrive::Error,
|
360
|
+
"The user doesn't have write permission to the spreadsheet: %p" % spreadsheet)
|
361
|
+
end
|
362
|
+
edit_url = edit_link['href']
|
363
|
+
xml << <<-EOS
|
375
364
|
<entry>
|
376
365
|
<batch:id>#{h(row)},#{h(col)}</batch:id>
|
377
366
|
<batch:operation type="update"/>
|
@@ -380,162 +369,157 @@ module GoogleDrive
|
|
380
369
|
href="#{h(edit_url)}"/>
|
381
370
|
<gs:cell row="#{h(row)}" col="#{h(col)}" inputValue="#{h(value)}"/>
|
382
371
|
</entry>
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
end
|
405
|
-
end
|
406
|
-
|
407
|
-
@modified.clear()
|
408
|
-
sent = true
|
409
|
-
|
372
|
+
EOS
|
373
|
+
end
|
374
|
+
xml << <<-"EOS"
|
375
|
+
</feed>
|
376
|
+
EOS
|
377
|
+
|
378
|
+
batch_url = concat_url(cells_feed_url, '/batch')
|
379
|
+
result = @session.request(
|
380
|
+
:post,
|
381
|
+
batch_url,
|
382
|
+
data: xml,
|
383
|
+
header: { 'Content-Type' => 'application/atom+xml;charset=utf-8', 'If-Match' => '*' })
|
384
|
+
result.css('entry').each do |entry|
|
385
|
+
interrupted = entry.css('batch|interrupted')[0]
|
386
|
+
if interrupted
|
387
|
+
fail(GoogleDrive::Error, 'Update has failed: %s' %
|
388
|
+
interrupted['reason'])
|
389
|
+
end
|
390
|
+
unless entry.css('batch|status').first['code'] =~ /^2/
|
391
|
+
fail(GoogleDrive::Error, 'Updating cell %s has failed: %s' %
|
392
|
+
[entry.css('id').text, entry.css('batch|status')[0]['reason']])
|
410
393
|
end
|
411
|
-
|
412
|
-
return sent
|
413
|
-
|
414
394
|
end
|
415
395
|
|
416
|
-
|
417
|
-
|
418
|
-
save()
|
419
|
-
reload()
|
420
|
-
end
|
396
|
+
@modified.clear
|
397
|
+
sent = true
|
421
398
|
|
422
|
-
|
423
|
-
def delete()
|
424
|
-
ws_doc = @session.request(:get, self.worksheet_feed_url)
|
425
|
-
edit_url = ws_doc.css("link[rel='edit']")[0]["href"]
|
426
|
-
@session.request(:delete, edit_url)
|
427
|
-
end
|
399
|
+
end
|
428
400
|
|
429
|
-
|
430
|
-
|
431
|
-
return !@modified.empty?
|
432
|
-
end
|
401
|
+
sent
|
402
|
+
end
|
433
403
|
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
# Provides access to cells using column names, assuming the first row contains column
|
441
|
-
# names. Returned object is GoogleDrive::List which you can use mostly as
|
442
|
-
# Array of Hash.
|
443
|
-
#
|
444
|
-
# e.g. Assuming the first row is ["x", "y"]:
|
445
|
-
# worksheet.list[0]["x"] #=> "1" # i.e. worksheet[2, 1]
|
446
|
-
# worksheet.list[0]["y"] #=> "2" # i.e. worksheet[2, 2]
|
447
|
-
# worksheet.list[1]["x"] = "3" # i.e. worksheet[3, 1] = "3"
|
448
|
-
# worksheet.list[1]["y"] = "4" # i.e. worksheet[3, 2] = "4"
|
449
|
-
# worksheet.list.push({"x" => "5", "y" => "6"})
|
450
|
-
#
|
451
|
-
# Note that update is not sent to the server until you call save().
|
452
|
-
def list
|
453
|
-
return @list ||= List.new(self)
|
454
|
-
end
|
455
|
-
|
456
|
-
# Returns a [row, col] pair for a cell name string.
|
457
|
-
# e.g.
|
458
|
-
# worksheet.cell_name_to_row_col("C2") #=> [2, 3]
|
459
|
-
def cell_name_to_row_col(cell_name)
|
460
|
-
if !cell_name.is_a?(String)
|
461
|
-
raise(ArgumentError, "Cell name must be a string: %p" % cell_name)
|
462
|
-
end
|
463
|
-
if !(cell_name.upcase =~ /^([A-Z]+)(\d+)$/)
|
464
|
-
raise(ArgumentError,
|
465
|
-
"Cell name must be only letters followed by digits with no spaces in between: %p" %
|
466
|
-
cell_name)
|
467
|
-
end
|
468
|
-
col = 0
|
469
|
-
$1.each_byte() do |b|
|
470
|
-
# 0x41: "A"
|
471
|
-
col = col * 26 + (b - 0x41 + 1)
|
472
|
-
end
|
473
|
-
row = $2.to_i()
|
474
|
-
return [row, col]
|
475
|
-
end
|
404
|
+
# Calls save() and reload().
|
405
|
+
def synchronize
|
406
|
+
save
|
407
|
+
reload
|
408
|
+
end
|
476
409
|
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
481
|
-
|
482
|
-
|
483
|
-
private
|
484
|
-
|
485
|
-
def set_worksheet_feed_entry(entry)
|
486
|
-
@worksheet_feed_entry = entry
|
487
|
-
@title = entry.css("title").text
|
488
|
-
@updated = Time.parse(entry.css("updated").text)
|
489
|
-
@meta_modified = false
|
490
|
-
end
|
410
|
+
# Deletes this worksheet. Deletion takes effect right away without calling save().
|
411
|
+
def delete
|
412
|
+
ws_doc = @session.request(:get, worksheet_feed_url)
|
413
|
+
edit_url = ws_doc.css("link[rel='edit']")[0]['href']
|
414
|
+
@session.request(:delete, edit_url)
|
415
|
+
end
|
491
416
|
|
492
|
-
|
493
|
-
|
494
|
-
|
495
|
-
|
496
|
-
@max_cols = doc.css("gs|colCount").text.to_i()
|
497
|
-
|
498
|
-
@num_cols = nil
|
499
|
-
@num_rows = nil
|
500
|
-
|
501
|
-
@cells = {}
|
502
|
-
@input_values = {}
|
503
|
-
@numeric_values = {}
|
504
|
-
doc.css("feed > entry").each() do |entry|
|
505
|
-
cell = entry.css("gs|cell")[0]
|
506
|
-
row = cell["row"].to_i()
|
507
|
-
col = cell["col"].to_i()
|
508
|
-
@cells[[row, col]] = cell.inner_text
|
509
|
-
@input_values[[row, col]] = cell["inputValue"] || cell.inner_text
|
510
|
-
numeric_value = cell["numericValue"]
|
511
|
-
@numeric_values[[row, col]] = numeric_value ? numeric_value.to_f() : nil
|
512
|
-
end
|
513
|
-
@modified.clear()
|
417
|
+
# Returns true if you have changes made by []= which haven't been saved.
|
418
|
+
def dirty?
|
419
|
+
!@modified.empty?
|
420
|
+
end
|
514
421
|
|
515
|
-
|
422
|
+
# List feed URL of the worksheet.
|
423
|
+
def list_feed_url
|
424
|
+
@worksheet_feed_entry.css(
|
425
|
+
"link[rel='http://schemas.google.com/spreadsheets/2006#listfeed']")[0]['href']
|
426
|
+
end
|
516
427
|
|
517
|
-
|
518
|
-
|
519
|
-
|
520
|
-
|
521
|
-
|
522
|
-
|
523
|
-
|
524
|
-
|
525
|
-
|
526
|
-
|
527
|
-
|
528
|
-
|
529
|
-
|
530
|
-
|
531
|
-
|
532
|
-
|
533
|
-
|
534
|
-
|
535
|
-
|
536
|
-
|
537
|
-
|
428
|
+
# Provides access to cells using column names, assuming the first row contains column
|
429
|
+
# names. Returned object is GoogleDrive::List which you can use mostly as
|
430
|
+
# Array of Hash.
|
431
|
+
#
|
432
|
+
# e.g. Assuming the first row is ["x", "y"]:
|
433
|
+
# worksheet.list[0]["x"] #=> "1" # i.e. worksheet[2, 1]
|
434
|
+
# worksheet.list[0]["y"] #=> "2" # i.e. worksheet[2, 2]
|
435
|
+
# worksheet.list[1]["x"] = "3" # i.e. worksheet[3, 1] = "3"
|
436
|
+
# worksheet.list[1]["y"] = "4" # i.e. worksheet[3, 2] = "4"
|
437
|
+
# worksheet.list.push({"x" => "5", "y" => "6"})
|
438
|
+
#
|
439
|
+
# Note that update is not sent to the server until you call save().
|
440
|
+
def list
|
441
|
+
@list ||= List.new(self)
|
442
|
+
end
|
443
|
+
|
444
|
+
# Returns a [row, col] pair for a cell name string.
|
445
|
+
# e.g.
|
446
|
+
# worksheet.cell_name_to_row_col("C2") #=> [2, 3]
|
447
|
+
def cell_name_to_row_col(cell_name)
|
448
|
+
unless cell_name.is_a?(String)
|
449
|
+
fail(ArgumentError, 'Cell name must be a string: %p' % cell_name)
|
450
|
+
end
|
451
|
+
unless cell_name.upcase =~ /^([A-Z]+)(\d+)$/
|
452
|
+
fail(ArgumentError,
|
453
|
+
'Cell name must be only letters followed by digits with no spaces in between: %p' %
|
454
|
+
cell_name)
|
455
|
+
end
|
456
|
+
col = 0
|
457
|
+
Regexp.last_match(1).each_byte do |b|
|
458
|
+
# 0x41: "A"
|
459
|
+
col = col * 26 + (b - 0x41 + 1)
|
460
|
+
end
|
461
|
+
row = Regexp.last_match(2).to_i
|
462
|
+
[row, col]
|
463
|
+
end
|
464
|
+
|
465
|
+
def inspect
|
466
|
+
fields = { worksheet_feed_url: worksheet_feed_url }
|
467
|
+
fields[:title] = @title if @title
|
468
|
+
"\#<%p %s>" % [self.class, fields.map { |k, v| '%s=%p' % [k, v] }.join(', ')]
|
469
|
+
end
|
470
|
+
|
471
|
+
private
|
472
|
+
|
473
|
+
def set_worksheet_feed_entry(entry)
|
474
|
+
@worksheet_feed_entry = entry
|
475
|
+
@title = entry.css('title').text
|
476
|
+
@updated = Time.parse(entry.css('updated').text)
|
477
|
+
@meta_modified = false
|
478
|
+
end
|
479
|
+
|
480
|
+
def reload_cells
|
481
|
+
doc = @session.request(:get, cells_feed_url)
|
482
|
+
@max_rows = doc.css('gs|rowCount').text.to_i
|
483
|
+
@max_cols = doc.css('gs|colCount').text.to_i
|
484
|
+
|
485
|
+
@num_cols = nil
|
486
|
+
@num_rows = nil
|
487
|
+
|
488
|
+
@cells = {}
|
489
|
+
@input_values = {}
|
490
|
+
@numeric_values = {}
|
491
|
+
doc.css('feed > entry').each do |entry|
|
492
|
+
cell = entry.css('gs|cell')[0]
|
493
|
+
row = cell['row'].to_i
|
494
|
+
col = cell['col'].to_i
|
495
|
+
@cells[[row, col]] = cell.inner_text
|
496
|
+
@input_values[[row, col]] = cell['inputValue'] || cell.inner_text
|
497
|
+
numeric_value = cell['numericValue']
|
498
|
+
@numeric_values[[row, col]] = numeric_value ? numeric_value.to_f : nil
|
499
|
+
end
|
500
|
+
@modified.clear
|
501
|
+
end
|
502
|
+
|
503
|
+
def parse_cell_args(args)
|
504
|
+
if args.size == 1 && args[0].is_a?(String)
|
505
|
+
return cell_name_to_row_col(args[0])
|
506
|
+
elsif args.size == 2 && args[0].is_a?(Integer) && args[1].is_a?(Integer)
|
507
|
+
if args[0] >= 1 && args[1] >= 1
|
508
|
+
return args
|
509
|
+
else
|
510
|
+
fail(ArgumentError,
|
511
|
+
'Row/col must be >= 1 (1-origin), but are %d/%d' % [args[0], args[1]])
|
512
|
+
end
|
513
|
+
else
|
514
|
+
fail(ArgumentError,
|
515
|
+
"Arguments must be either one String or two Integer's, but are %p" % [args])
|
516
|
+
end
|
517
|
+
end
|
538
518
|
|
519
|
+
def validate_cell_value(value)
|
520
|
+
if value.include?("\x1a")
|
521
|
+
fail(ArgumentError, 'Contains invalid character \\x1a for xml: %p' % value)
|
522
|
+
end
|
539
523
|
end
|
540
|
-
|
524
|
+
end
|
541
525
|
end
|