google_drive 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,220 @@
1
+ # Author: Hiroshi Ichikawa <http://gimite.net/>
2
+ # The license of this source is "New BSD Licence"
3
+
4
+ require "google_drive/util"
5
+ require "google_drive/error"
6
+ require "google_drive/worksheet"
7
+ require "google_drive/table"
8
+ require "google_drive/acl"
9
+ require "google_drive/file"
10
+
11
+
12
+ module GoogleDrive
13
+
14
+ # A spreadsheet.
15
+ #
16
+ # Use methods in GoogleDrive::Session to get GoogleDrive::Spreadsheet object.
17
+ class Spreadsheet < GoogleDrive::File
18
+
19
+ include(Util)
20
+
21
+ SUPPORTED_EXPORT_FORMAT = Set.new(["xls", "csv", "pdf", "ods", "tsv", "html"])
22
+
23
+ def initialize(session, worksheets_feed_url, title = nil) #:nodoc:
24
+ super(session, nil)
25
+ @worksheets_feed_url = worksheets_feed_url
26
+ @title = title
27
+ end
28
+
29
+ # URL of worksheet-based feed of the spreadsheet.
30
+ attr_reader(:worksheets_feed_url)
31
+
32
+ # Title of the spreadsheet.
33
+ #
34
+ # Set <tt>params[:reload]</tt> to true to force reloading the title.
35
+ def title(params = {})
36
+ if !@title || params[:reload]
37
+ @title = spreadsheet_feed_entry(params).css("title").text
38
+ end
39
+ return @title
40
+ end
41
+
42
+ # Key of the spreadsheet.
43
+ def key
44
+ if !(@worksheets_feed_url =~
45
+ %r{^https?://spreadsheets.google.com/feeds/worksheets/(.*)/private/.*$})
46
+ raise(GoogleDrive::Error,
47
+ "Worksheets feed URL is in unknown format: #{@worksheets_feed_url}")
48
+ end
49
+ return $1
50
+ end
51
+
52
+ # Spreadsheet feed URL of the spreadsheet.
53
+ def spreadsheet_feed_url
54
+ return "https://spreadsheets.google.com/feeds/spreadsheets/private/full/#{self.key}"
55
+ end
56
+
57
+ # URL which you can open the spreadsheet in a Web browser with.
58
+ #
59
+ # e.g. "http://spreadsheets.google.com/ccc?key=pz7XtlQC-PYx-jrVMJErTcg"
60
+ def human_url
61
+ # Uses Document feed because Spreadsheet feed returns wrong URL for Apps account.
62
+ return self.document_feed_entry.css("link[rel='alternate']")[0]["href"]
63
+ end
64
+
65
+ # DEPRECATED: Table and Record feeds are deprecated and they will not be available after
66
+ # March 2012.
67
+ #
68
+ # Tables feed URL of the spreadsheet.
69
+ def tables_feed_url
70
+ warn(
71
+ "DEPRECATED: Google Spreadsheet Table and Record feeds are deprecated and they " +
72
+ "will not be available after March 2012.")
73
+ return "https://spreadsheets.google.com/feeds/#{self.key}/tables"
74
+ end
75
+
76
+ # URL of feed used in document list feed API.
77
+ def document_feed_url
78
+ return "https://docs.google.com/feeds/documents/private/full/spreadsheet%3A#{self.key}"
79
+ end
80
+
81
+ # <entry> element of spreadsheet feed as Nokogiri::XML::Element.
82
+ #
83
+ # Set <tt>params[:reload]</tt> to true to force reloading the feed.
84
+ def spreadsheet_feed_entry(params = {})
85
+ if !@spreadsheet_feed_entry || params[:reload]
86
+ @spreadsheet_feed_entry =
87
+ @session.request(:get, self.spreadsheet_feed_url).css("entry")[0]
88
+ end
89
+ return @spreadsheet_feed_entry
90
+ end
91
+
92
+ # <entry> element of document list feed as Nokogiri::XML::Element.
93
+ #
94
+ # Set <tt>params[:reload]</tt> to true to force reloading the feed.
95
+ def document_feed_entry(params = {})
96
+ if !@document_feed_entry || params[:reload]
97
+ @document_feed_entry =
98
+ @session.request(:get, self.document_feed_url, :auth => :writely).css("entry")[0]
99
+ end
100
+ return @document_feed_entry
101
+ end
102
+
103
+ # Creates copy of this spreadsheet with the given title.
104
+ def duplicate(new_title = nil)
105
+ new_title ||= (self.title ? "Copy of " + self.title : "Untitled")
106
+ post_url = "https://docs.google.com/feeds/default/private/full/"
107
+ header = {"GData-Version" => "3.0", "Content-Type" => "application/atom+xml"}
108
+ xml = <<-"EOS"
109
+ <entry xmlns='http://www.w3.org/2005/Atom'>
110
+ <id>#{h(self.document_feed_url)}</id>
111
+ <title>#{h(new_title)}</title>
112
+ </entry>
113
+ EOS
114
+ doc = @session.request(
115
+ :post, post_url, :data => xml, :header => header, :auth => :writely)
116
+ ss_url = doc.css(
117
+ "link[rel='http://schemas.google.com/spreadsheets/2006#worksheetsfeed']")[0]["href"]
118
+ return Spreadsheet.new(@session, ss_url, new_title)
119
+ end
120
+
121
+ # Exports the spreadsheet in +format+ and returns it as String.
122
+ #
123
+ # +format+ can be either "xls", "csv", "pdf", "ods", "tsv" or "html".
124
+ # In format such as "csv", only the worksheet specified with +worksheet_index+ is
125
+ # exported.
126
+ def export_as_string(format, worksheet_index = nil)
127
+ gid_param = worksheet_index ? "&gid=#{worksheet_index}" : ""
128
+ url =
129
+ "https://spreadsheets.google.com/feeds/download/spreadsheets/Export" +
130
+ "?key=#{key}&exportFormat=#{format}#{gid_param}"
131
+ return @session.request(:get, url, :response_type => :raw)
132
+ end
133
+
134
+ # Exports the spreadsheet in +format+ as a local file.
135
+ #
136
+ # +format+ can be either "xls", "csv", "pdf", "ods", "tsv" or "html".
137
+ # If +format+ is nil, it is guessed from the file name.
138
+ # In format such as "csv", only the worksheet specified with +worksheet_index+ is exported.
139
+ #
140
+ # e.g.
141
+ # spreadsheet.export_as_file("hoge.ods")
142
+ # spreadsheet.export_as_file("hoge.csv", nil, 0)
143
+ def export_as_file(local_path, format = nil, worksheet_index = nil)
144
+ if !format
145
+ format = ::File.extname(local_path).gsub(/^\./, "")
146
+ if !SUPPORTED_EXPORT_FORMAT.include?(format)
147
+ raise(ArgumentError,
148
+ ("Cannot guess format from the file name: %s\n" +
149
+ "Specify format argument explicitly.") %
150
+ local_path)
151
+ end
152
+ end
153
+ open(local_path, "wb") do |f|
154
+ f.write(export_as_string(format, worksheet_index))
155
+ end
156
+ end
157
+
158
+ # Returns worksheets of the spreadsheet as array of GoogleDrive::Worksheet.
159
+ def worksheets
160
+ doc = @session.request(:get, @worksheets_feed_url)
161
+ if doc.root.name != "feed"
162
+ raise(GoogleDrive::Error,
163
+ "%s doesn't look like a worksheets feed URL because its root is not <feed>." %
164
+ @worksheets_feed_url)
165
+ end
166
+ result = []
167
+ doc.css("entry").each() do |entry|
168
+ title = entry.css("title").text
169
+ url = entry.css(
170
+ "link[rel='http://schemas.google.com/spreadsheets/2006#cellsfeed']")[0]["href"]
171
+ result.push(Worksheet.new(@session, self, url, title))
172
+ end
173
+ return result.freeze()
174
+ end
175
+
176
+ # Returns a GoogleDrive::Worksheet with the given title in the spreadsheet.
177
+ #
178
+ # Returns nil if not found. Returns the first one when multiple worksheets with the
179
+ # title are found.
180
+ def worksheet_by_title(title)
181
+ return self.worksheets.find(){ |ws| ws.title == title }
182
+ end
183
+
184
+ # Adds a new worksheet to the spreadsheet. Returns added GoogleDrive::Worksheet.
185
+ def add_worksheet(title, max_rows = 100, max_cols = 20)
186
+ xml = <<-"EOS"
187
+ <entry xmlns='http://www.w3.org/2005/Atom'
188
+ xmlns:gs='http://schemas.google.com/spreadsheets/2006'>
189
+ <title>#{h(title)}</title>
190
+ <gs:rowCount>#{h(max_rows)}</gs:rowCount>
191
+ <gs:colCount>#{h(max_cols)}</gs:colCount>
192
+ </entry>
193
+ EOS
194
+ doc = @session.request(:post, @worksheets_feed_url, :data => xml)
195
+ url = doc.css(
196
+ "link[rel='http://schemas.google.com/spreadsheets/2006#cellsfeed']")[0]["href"]
197
+ return Worksheet.new(@session, self, url, title)
198
+ end
199
+
200
+ # DEPRECATED: Table and Record feeds are deprecated and they will not be available after
201
+ # March 2012.
202
+ #
203
+ # Returns list of tables in the spreadsheet.
204
+ def tables
205
+ warn(
206
+ "DEPRECATED: Google Spreadsheet Table and Record feeds are deprecated and they " +
207
+ "will not be available after March 2012.")
208
+ doc = @session.request(:get, self.tables_feed_url)
209
+ return doc.css("entry").map(){ |e| Table.new(@session, e) }.freeze()
210
+ end
211
+
212
+ def inspect
213
+ fields = {:worksheets_feed_url => self.worksheets_feed_url}
214
+ fields[:title] = @title if @title
215
+ return "\#<%p %s>" % [self.class, fields.map(){ |k, v| "%s=%p" % [k, v] }.join(", ")]
216
+ end
217
+
218
+ end
219
+
220
+ end
@@ -0,0 +1,60 @@
1
+ # Author: Hiroshi Ichikawa <http://gimite.net/>
2
+ # The license of this source is "New BSD Licence"
3
+
4
+ require "google_drive/util"
5
+ require "google_drive/error"
6
+ require "google_drive/record"
7
+
8
+
9
+ module GoogleDrive
10
+
11
+ # DEPRECATED: Table and Record feeds are deprecated and they will not be available after
12
+ # March 2012.
13
+ #
14
+ # Use GoogleDrive::Worksheet#add_table to create table.
15
+ # Use GoogleDrive::Worksheet#tables to get GoogleDrive::Table objects.
16
+ class Table
17
+
18
+ include(Util)
19
+
20
+ def initialize(session, entry) #:nodoc:
21
+ @columns = {}
22
+ @worksheet_title = entry.css("gs|worksheet")[0]["name"]
23
+ @records_url = entry.css("content")[0]["src"]
24
+ @edit_url = entry.css("link[rel='edit']")[0]["href"]
25
+ @session = session
26
+ end
27
+
28
+ # Title of the worksheet the table belongs to.
29
+ attr_reader(:worksheet_title)
30
+
31
+ # Adds a record.
32
+ def add_record(values)
33
+ fields = ""
34
+ values.each() do |name, value|
35
+ fields += "<gs:field name='#{h(name)}'>#{h(value)}</gs:field>"
36
+ end
37
+ xml =<<-EOS
38
+ <entry
39
+ xmlns="http://www.w3.org/2005/Atom"
40
+ xmlns:gs="http://schemas.google.com/spreadsheets/2006">
41
+ #{fields}
42
+ </entry>
43
+ EOS
44
+ @session.request(:post, @records_url, :data => xml)
45
+ end
46
+
47
+ # Returns records in the table.
48
+ def records
49
+ doc = @session.request(:get, @records_url)
50
+ return doc.css("entry").map(){ |e| Record.new(@session, e) }
51
+ end
52
+
53
+ # Deletes this table. Deletion takes effect right away without calling save().
54
+ def delete
55
+ @session.request(:delete, @edit_url, :header => {"If-Match" => "*"})
56
+ end
57
+
58
+ end
59
+
60
+ end
@@ -0,0 +1,55 @@
1
+ # Author: Hiroshi Ichikawa <http://gimite.net/>
2
+ # The license of this source is "New BSD Licence"
3
+
4
+ require "cgi"
5
+
6
+
7
+ module GoogleDrive
8
+
9
+ module Util #:nodoc:
10
+
11
+ EXT_TO_CONTENT_TYPE = {
12
+ ".csv" =>"text/csv",
13
+ ".tsv" =>"text/tab-separated-values",
14
+ ".tab" =>"text/tab-separated-values",
15
+ ".doc" =>"application/msword",
16
+ ".docx" => "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
17
+ ".ods" =>"application/x-vnd.oasis.opendocument.spreadsheet",
18
+ ".odt" =>"application/vnd.oasis.opendocument.text",
19
+ ".rtf" =>"application/rtf",
20
+ ".sxw" =>"application/vnd.sun.xml.writer",
21
+ ".txt" =>"text/plain",
22
+ ".xls" =>"application/vnd.ms-excel",
23
+ ".xlsx" => "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
24
+ ".pdf" =>"application/pdf",
25
+ ".png" =>"image/png",
26
+ ".ppt" =>"application/vnd.ms-powerpoint",
27
+ ".pps" =>"application/vnd.ms-powerpoint",
28
+ ".htm" =>"text/html",
29
+ ".html" =>"text/html",
30
+ ".zip" =>"application/zip",
31
+ ".swf" =>"application/x-shockwave-flash",
32
+ }
33
+
34
+ module_function
35
+
36
+ def encode_query(params)
37
+ return params.map(){ |k, v| CGI.escape(k.to_s()) + "=" + CGI.escape(v.to_s()) }.join("&")
38
+ end
39
+
40
+ def concat_url(url, piece)
41
+ (url_base, url_query) = url.split(/\?/, 2)
42
+ (piece_base, piece_query) = piece.split(/\?/, 2)
43
+ result_query = [url_query, piece_query].select(){ |s| s && !s.empty? }.join("&")
44
+ return (url_base || "") +
45
+ (piece_base || "") +
46
+ (result_query.empty? ? "" : "?#{result_query}")
47
+ end
48
+
49
+ def h(str)
50
+ return CGI.escapeHTML(str.to_s())
51
+ end
52
+
53
+ end
54
+
55
+ end
@@ -0,0 +1,445 @@
1
+ # Author: Hiroshi Ichikawa <http://gimite.net/>
2
+ # The license of this source is "New BSD Licence"
3
+
4
+ require "set"
5
+
6
+ require "google_drive/util"
7
+ require "google_drive/error"
8
+ require "google_drive/table"
9
+ require "google_drive/list"
10
+
11
+
12
+ module GoogleDrive
13
+
14
+ # A worksheet (i.e. a tab) in a spreadsheet.
15
+ # Use GoogleDrive::Spreadsheet#worksheets to get GoogleDrive::Worksheet object.
16
+ class Worksheet
17
+
18
+ include(Util)
19
+
20
+ def initialize(session, spreadsheet, cells_feed_url, title = nil) #:nodoc:
21
+
22
+ @session = session
23
+ @spreadsheet = spreadsheet
24
+ @cells_feed_url = cells_feed_url
25
+ @title = title
26
+
27
+ @cells = nil
28
+ @input_values = nil
29
+ @modified = Set.new()
30
+ @list = nil
31
+
32
+ end
33
+
34
+ # URL of cell-based feed of the worksheet.
35
+ attr_reader(:cells_feed_url)
36
+
37
+ # URL of worksheet feed URL of the worksheet.
38
+ def worksheet_feed_url
39
+ # I don't know good way to get worksheet feed URL from cells feed URL.
40
+ # Probably it would be cleaner to keep worksheet feed URL and get cells feed URL
41
+ # from it.
42
+ if !(@cells_feed_url =~
43
+ %r{^https?://spreadsheets.google.com/feeds/cells/(.*)/(.*)/private/full((\?.*)?)$})
44
+ raise(GoogleDrive::Error,
45
+ "Cells feed URL is in unknown format: #{@cells_feed_url}")
46
+ end
47
+ return "https://spreadsheets.google.com/feeds/worksheets/#{$1}/private/full/#{$2}#{$3}"
48
+ end
49
+
50
+ # GoogleDrive::Spreadsheet which this worksheet belongs to.
51
+ def spreadsheet
52
+ if !@spreadsheet
53
+ if !(@cells_feed_url =~
54
+ %r{^https?://spreadsheets.google.com/feeds/cells/(.*)/(.*)/private/full(\?.*)?$})
55
+ raise(GoogleDrive::Error,
56
+ "Cells feed URL is in unknown format: #{@cells_feed_url}")
57
+ end
58
+ @spreadsheet = @session.spreadsheet_by_key($1)
59
+ end
60
+ return @spreadsheet
61
+ end
62
+
63
+ # Returns content of the cell as String. Arguments must be either
64
+ # (row number, column number) or cell name. Top-left cell is [1, 1].
65
+ #
66
+ # e.g.
67
+ # worksheet[2, 1] #=> "hoge"
68
+ # worksheet["A2"] #=> "hoge"
69
+ def [](*args)
70
+ (row, col) = parse_cell_args(args)
71
+ return self.cells[[row, col]] || ""
72
+ end
73
+
74
+ # Updates content of the cell.
75
+ # Arguments in the bracket must be either (row number, column number) or cell name.
76
+ # Note that update is not sent to the server until you call save().
77
+ # Top-left cell is [1, 1].
78
+ #
79
+ # e.g.
80
+ # worksheet[2, 1] = "hoge"
81
+ # worksheet["A2"] = "hoge"
82
+ # worksheet[1, 3] = "=A1+B1"
83
+ def []=(*args)
84
+ (row, col) = parse_cell_args(args[0...-1])
85
+ value = args[-1].to_s()
86
+ reload() if !@cells
87
+ @cells[[row, col]] = value
88
+ @input_values[[row, col]] = value
89
+ @modified.add([row, col])
90
+ self.max_rows = row if row > @max_rows
91
+ self.max_cols = col if col > @max_cols
92
+ end
93
+
94
+ # Updates cells in a rectangle area by a two-dimensional Array.
95
+ # +top_row+ and +left_col+ specifies the top-left corner of the area.
96
+ #
97
+ # e.g.
98
+ # worksheet.update_cells(2, 3, [["1", "2"], ["3", "4"]])
99
+ def update_cells(top_row, left_col, darray)
100
+ darray.each_with_index() do |array, y|
101
+ array.each_with_index() do |value, x|
102
+ self[top_row + y, left_col + x] = value
103
+ end
104
+ end
105
+ end
106
+
107
+ # Returns the value or the formula of the cell. Arguments must be either
108
+ # (row number, column number) or cell name. Top-left cell is [1, 1].
109
+ #
110
+ # If user input "=A1+B1" to cell [1, 3]:
111
+ # worksheet[1, 3] #=> "3" for example
112
+ # worksheet.input_value(1, 3) #=> "=RC[-2]+RC[-1]"
113
+ def input_value(*args)
114
+ (row, col) = parse_cell_args(args)
115
+ reload() if !@cells
116
+ return @input_values[[row, col]] || ""
117
+ end
118
+
119
+ # Row number of the bottom-most non-empty row.
120
+ def num_rows
121
+ reload() if !@cells
122
+ return @input_values.select(){ |(r, c), v| !v.empty? }.map(){ |(r, c), v| r }.max || 0
123
+ end
124
+
125
+ # Column number of the right-most non-empty column.
126
+ def num_cols
127
+ reload() if !@cells
128
+ return @input_values.select(){ |(r, c), v| !v.empty? }.map(){ |(r, c), v| c }.max || 0
129
+ end
130
+
131
+ # Number of rows including empty rows.
132
+ def max_rows
133
+ reload() if !@cells
134
+ return @max_rows
135
+ end
136
+
137
+ # Updates number of rows.
138
+ # Note that update is not sent to the server until you call save().
139
+ def max_rows=(rows)
140
+ reload() if !@cells
141
+ @max_rows = rows
142
+ @meta_modified = true
143
+ end
144
+
145
+ # Number of columns including empty columns.
146
+ def max_cols
147
+ reload() if !@cells
148
+ return @max_cols
149
+ end
150
+
151
+ # Updates number of columns.
152
+ # Note that update is not sent to the server until you call save().
153
+ def max_cols=(cols)
154
+ reload() if !@cells
155
+ @max_cols = cols
156
+ @meta_modified = true
157
+ end
158
+
159
+ # Title of the worksheet (shown as tab label in Web interface).
160
+ def title
161
+ reload() if !@title
162
+ return @title
163
+ end
164
+
165
+ # Updates title of the worksheet.
166
+ # Note that update is not sent to the server until you call save().
167
+ def title=(title)
168
+ reload() if !@cells
169
+ @title = title
170
+ @meta_modified = true
171
+ end
172
+
173
+ def cells #:nodoc:
174
+ reload() if !@cells
175
+ return @cells
176
+ end
177
+
178
+ # An array of spreadsheet rows. Each row contains an array of
179
+ # columns. Note that resulting array is 0-origin so
180
+ # worksheet.rows[0][0] == worksheet[1, 1].
181
+ def rows(skip = 0)
182
+ nc = self.num_cols
183
+ result = ((1 + skip)..self.num_rows).map() do |row|
184
+ (1..nc).map(){ |col| self[row, col] }.freeze()
185
+ end
186
+ return result.freeze()
187
+ end
188
+
189
+ # Reloads content of the worksheets from the server.
190
+ # Note that changes you made by []= etc. is discarded if you haven't called save().
191
+ def reload()
192
+
193
+ doc = @session.request(:get, @cells_feed_url)
194
+ @max_rows = doc.css("gs|rowCount").text.to_i()
195
+ @max_cols = doc.css("gs|colCount").text.to_i()
196
+ @title = doc.css("feed > title")[0].text
197
+
198
+ @cells = {}
199
+ @input_values = {}
200
+ doc.css("feed > entry").each() do |entry|
201
+ cell = entry.css("gs|cell")[0]
202
+ row = cell["row"].to_i()
203
+ col = cell["col"].to_i()
204
+ @cells[[row, col]] = cell.inner_text
205
+ @input_values[[row, col]] = cell["inputValue"]
206
+ end
207
+ @modified.clear()
208
+ @meta_modified = false
209
+ return true
210
+
211
+ end
212
+
213
+ # Saves your changes made by []=, etc. to the server.
214
+ def save()
215
+
216
+ sent = false
217
+
218
+ if @meta_modified
219
+
220
+ ws_doc = @session.request(:get, self.worksheet_feed_url)
221
+ edit_url = ws_doc.css("link[rel='edit']")[0]["href"]
222
+ xml = <<-"EOS"
223
+ <entry xmlns='http://www.w3.org/2005/Atom'
224
+ xmlns:gs='http://schemas.google.com/spreadsheets/2006'>
225
+ <title>#{h(self.title)}</title>
226
+ <gs:rowCount>#{h(self.max_rows)}</gs:rowCount>
227
+ <gs:colCount>#{h(self.max_cols)}</gs:colCount>
228
+ </entry>
229
+ EOS
230
+
231
+ @session.request(:put, edit_url, :data => xml)
232
+
233
+ @meta_modified = false
234
+ sent = true
235
+
236
+ end
237
+
238
+ if !@modified.empty?
239
+
240
+ # Gets id and edit URL for each cell.
241
+ # Note that return-empty=true is required to get those info for empty cells.
242
+ cell_entries = {}
243
+ rows = @modified.map(){ |r, c| r }
244
+ cols = @modified.map(){ |r, c| c }
245
+ url = concat_url(@cells_feed_url,
246
+ "?return-empty=true&min-row=#{rows.min}&max-row=#{rows.max}" +
247
+ "&min-col=#{cols.min}&max-col=#{cols.max}")
248
+ doc = @session.request(:get, url)
249
+
250
+ doc.css("entry").each() do |entry|
251
+ row = entry.css("gs|cell")[0]["row"].to_i()
252
+ col = entry.css("gs|cell")[0]["col"].to_i()
253
+ cell_entries[[row, col]] = entry
254
+ end
255
+
256
+ # Updates cell values using batch operation.
257
+ # If the data is large, we split it into multiple operations, otherwise batch may fail.
258
+ @modified.each_slice(250) do |chunk|
259
+
260
+ xml = <<-EOS
261
+ <feed xmlns="http://www.w3.org/2005/Atom"
262
+ xmlns:batch="http://schemas.google.com/gdata/batch"
263
+ xmlns:gs="http://schemas.google.com/spreadsheets/2006">
264
+ <id>#{h(@cells_feed_url)}</id>
265
+ EOS
266
+ for row, col in chunk
267
+ value = @cells[[row, col]]
268
+ entry = cell_entries[[row, col]]
269
+ id = entry.css("id").text
270
+ edit_url = entry.css("link[rel='edit']")[0]["href"]
271
+ xml << <<-EOS
272
+ <entry>
273
+ <batch:id>#{h(row)},#{h(col)}</batch:id>
274
+ <batch:operation type="update"/>
275
+ <id>#{h(id)}</id>
276
+ <link rel="edit" type="application/atom+xml"
277
+ href="#{h(edit_url)}"/>
278
+ <gs:cell row="#{h(row)}" col="#{h(col)}" inputValue="#{h(value)}"/>
279
+ </entry>
280
+ EOS
281
+ end
282
+ xml << <<-"EOS"
283
+ </feed>
284
+ EOS
285
+
286
+ batch_url = concat_url(@cells_feed_url, "/batch")
287
+ result = @session.request(:post, batch_url, :data => xml)
288
+ result.css("atom|entry").each() do |entry|
289
+ interrupted = entry.css("batch|interrupted")[0]
290
+ if interrupted
291
+ raise(GoogleDrive::Error, "Update has failed: %s" %
292
+ interrupted["reason"])
293
+ end
294
+ if !(entry.css("batch|status").first["code"] =~ /^2/)
295
+ raise(GoogleDrive::Error, "Updating cell %s has failed: %s" %
296
+ [entry.css("atom|id").text, entry.css("batch|status")[0]["reason"]])
297
+ end
298
+ end
299
+
300
+ end
301
+
302
+ @modified.clear()
303
+ sent = true
304
+
305
+ end
306
+
307
+ return sent
308
+
309
+ end
310
+
311
+ # Calls save() and reload().
312
+ def synchronize()
313
+ save()
314
+ reload()
315
+ end
316
+
317
+ # Deletes this worksheet. Deletion takes effect right away without calling save().
318
+ def delete()
319
+ ws_doc = @session.request(:get, self.worksheet_feed_url)
320
+ edit_url = ws_doc.css("link[rel='edit']")[0]["href"]
321
+ @session.request(:delete, edit_url)
322
+ end
323
+
324
+ # Returns true if you have changes made by []= which haven't been saved.
325
+ def dirty?
326
+ return !@modified.empty?
327
+ end
328
+
329
+ # DEPRECATED: Table and Record feeds are deprecated and they will not be available after
330
+ # March 2012.
331
+ #
332
+ # Creates table for the worksheet and returns GoogleDrive::Table.
333
+ # See this document for details:
334
+ # http://code.google.com/intl/en/apis/spreadsheets/docs/3.0/developers_guide_protocol.html#TableFeeds
335
+ def add_table(table_title, summary, columns, options)
336
+
337
+ warn(
338
+ "DEPRECATED: Google Spreadsheet Table and Record feeds are deprecated and they " +
339
+ "will not be available after March 2012.")
340
+ default_options = { :header_row => 1, :num_rows => 0, :start_row => 2}
341
+ options = default_options.merge(options)
342
+
343
+ column_xml = ""
344
+ columns.each() do |index, name|
345
+ column_xml += "<gs:column index='#{h(index)}' name='#{h(name)}'/>\n"
346
+ end
347
+
348
+ xml = <<-"EOS"
349
+ <entry xmlns="http://www.w3.org/2005/Atom"
350
+ xmlns:gs="http://schemas.google.com/spreadsheets/2006">
351
+ <title type='text'>#{h(table_title)}</title>
352
+ <summary type='text'>#{h(summary)}</summary>
353
+ <gs:worksheet name='#{h(self.title)}' />
354
+ <gs:header row='#{options[:header_row]}' />
355
+ <gs:data numRows='#{options[:num_rows]}' startRow='#{options[:start_row]}'>
356
+ #{column_xml}
357
+ </gs:data>
358
+ </entry>
359
+ EOS
360
+
361
+ result = @session.request(:post, self.spreadsheet.tables_feed_url, :data => xml)
362
+ return Table.new(@session, result)
363
+
364
+ end
365
+
366
+ # DEPRECATED: Table and Record feeds are deprecated and they will not be available after
367
+ # March 2012.
368
+ #
369
+ # Returns list of tables for the workwheet.
370
+ def tables
371
+ warn(
372
+ "DEPRECATED: Google Spreadsheet Table and Record feeds are deprecated and they " +
373
+ "will not be available after March 2012.")
374
+ return self.spreadsheet.tables.select(){ |t| t.worksheet_title == self.title }
375
+ end
376
+
377
+ # List feed URL of the worksheet.
378
+ def list_feed_url
379
+ # Gets the worksheets metafeed.
380
+ entry = @session.request(:get, self.worksheet_feed_url)
381
+
382
+ # Gets the URL of list-based feed for the given spreadsheet.
383
+ return entry.css(
384
+ "link[rel='http://schemas.google.com/spreadsheets/2006#listfeed']")[0]["href"]
385
+ end
386
+
387
+ # Provides access to cells using column names, assuming the first row contains column
388
+ # names. Returned object is GoogleDrive::List which you can use mostly as
389
+ # Array of Hash.
390
+ #
391
+ # e.g. Assuming the first row is ["x", "y"]:
392
+ # worksheet.list[0]["x"] #=> "1" # i.e. worksheet[2, 1]
393
+ # worksheet.list[0]["y"] #=> "2" # i.e. worksheet[2, 2]
394
+ # worksheet.list[1]["x"] = "3" # i.e. worksheet[3, 1] = "3"
395
+ # worksheet.list[1]["y"] = "4" # i.e. worksheet[3, 2] = "4"
396
+ # worksheet.list.push({"x" => "5", "y" => "6"})
397
+ #
398
+ # Note that update is not sent to the server until you call save().
399
+ def list
400
+ return @list ||= List.new(self)
401
+ end
402
+
403
+ # Returns a [row, col] pair for a cell name string.
404
+ # e.g.
405
+ # worksheet.cell_name_to_row_col("C2") #=> [2, 3]
406
+ def cell_name_to_row_col(cell_name)
407
+ if !cell_name.is_a?(String)
408
+ raise(ArgumentError, "Cell name must be a string: %p" % cell_name)
409
+ end
410
+ if !(cell_name.upcase =~ /^([A-Z]+)(\d+)$/)
411
+ raise(ArgumentError,
412
+ "Cell name must be only letters followed by digits with no spaces in between: %p" %
413
+ cell_name)
414
+ end
415
+ col = 0
416
+ $1.each_byte() do |b|
417
+ # 0x41: "A"
418
+ col = col * 26 + (b - 0x41 + 1)
419
+ end
420
+ row = $2.to_i()
421
+ return [row, col]
422
+ end
423
+
424
+ def inspect
425
+ fields = {:worksheet_feed_url => self.worksheet_feed_url}
426
+ fields[:title] = @title if @title
427
+ return "\#<%p %s>" % [self.class, fields.map(){ |k, v| "%s=%p" % [k, v] }.join(", ")]
428
+ end
429
+
430
+ private
431
+
432
+ def parse_cell_args(args)
433
+ if args.size == 1 && args[0].is_a?(String)
434
+ return cell_name_to_row_col(args[0])
435
+ elsif args.size == 2 && args[0].is_a?(Integer) && args[1].is_a?(Integer)
436
+ return args
437
+ else
438
+ raise(ArgumentError,
439
+ "Arguments must be either one String or two Integer's, but are %p" % args)
440
+ end
441
+ end
442
+
443
+ end
444
+
445
+ end