google_drive 1.0.6 → 2.0.0.pre1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|