google-spreadsheet-ruby 0.1.8 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,298 @@
1
+ # Author: Hiroshi Ichikawa <http://gimite.net/>
2
+ # The license of this source is "New BSD Licence"
3
+
4
+ require "google_spreadsheet/util"
5
+ require "google_spreadsheet/error"
6
+ require "google_spreadsheet/worksheet"
7
+ require "google_spreadsheet/table"
8
+ require "google_spreadsheet/acl"
9
+
10
+
11
+ module GoogleSpreadsheet
12
+
13
+ # A spreadsheet.
14
+ #
15
+ # Use methods in GoogleSpreadsheet::Session to get GoogleSpreadsheet::Spreadsheet object.
16
+ class Spreadsheet
17
+
18
+ include(Util)
19
+
20
+ SUPPORTED_EXPORT_FORMAT = Set.new(["xls", "csv", "pdf", "ods", "tsv", "html"])
21
+
22
+ def initialize(session, worksheets_feed_url, title = nil) #:nodoc:
23
+ @session = session
24
+ @worksheets_feed_url = worksheets_feed_url
25
+ @title = title
26
+ @acl = nil
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 params[:reload] 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(GoogleSpreadsheet::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
+ # ACL feed URL of the spreadsheet.
82
+ def acl_feed_url
83
+ orig_acl_feed_url = document_feed_entry.css(
84
+ "gd|feedLink[rel='http://schemas.google.com/acl/2007#accessControlList']")[0]["href"]
85
+ case orig_acl_feed_url
86
+ when %r{^https?://docs.google.com/feeds/default/private/full/.*/acl$}
87
+ return orig_acl_feed_url
88
+ when %r{^https?://docs.google.com/feeds/acl/private/full/([^\?]*)(\?.*)?$}
89
+ # URL of old API version. Converts to v3 URL.
90
+ return "https://docs.google.com/feeds/default/private/full/#{$1}/acl"
91
+ else
92
+ raise(GoogleSpreadsheet::Error,
93
+ "ACL feed URL is in unknown format: #{orig_acl_feed_url}")
94
+ end
95
+ end
96
+
97
+ # <entry> element of spreadsheet feed as Nokogiri::XML::Element.
98
+ #
99
+ # Set params[:reload] to true to force reloading the feed.
100
+ def spreadsheet_feed_entry(params = {})
101
+ if !@spreadsheet_feed_entry || params[:reload]
102
+ @spreadsheet_feed_entry =
103
+ @session.request(:get, self.spreadsheet_feed_url).css("entry")[0]
104
+ end
105
+ return @spreadsheet_feed_entry
106
+ end
107
+
108
+ # <entry> element of document list feed as Nokogiri::XML::Element.
109
+ #
110
+ # Set params[:reload] to true to force reloading the feed.
111
+ def document_feed_entry(params = {})
112
+ if !@document_feed_entry || params[:reload]
113
+ @document_feed_entry =
114
+ @session.request(:get, self.document_feed_url, :auth => :writely).css("entry")[0]
115
+ end
116
+ return @document_feed_entry
117
+ end
118
+
119
+ # Creates copy of this spreadsheet with the given title.
120
+ def duplicate(new_title = nil)
121
+ new_title ||= (self.title ? "Copy of " + self.title : "Untitled")
122
+ post_url = "https://docs.google.com/feeds/default/private/full/"
123
+ header = {"GData-Version" => "3.0", "Content-Type" => "application/atom+xml"}
124
+ xml = <<-"EOS"
125
+ <entry xmlns='http://www.w3.org/2005/Atom'>
126
+ <id>#{h(self.document_feed_url)}</id>
127
+ <title>#{h(new_title)}</title>
128
+ </entry>
129
+ EOS
130
+ doc = @session.request(
131
+ :post, post_url, :data => xml, :header => header, :auth => :writely)
132
+ ss_url = doc.css(
133
+ "link[rel='http://schemas.google.com/spreadsheets/2006#worksheetsfeed']")[0]["href"]
134
+ return Spreadsheet.new(@session, ss_url, new_title)
135
+ end
136
+
137
+ # If +permanent+ is +false+, moves the spreadsheet to the trash.
138
+ # If +permanent+ is +true+, deletes the spreadsheet permanently.
139
+ def delete(permanent = false)
140
+ @session.request(:delete,
141
+ self.document_feed_url + (permanent ? "?delete=true" : ""),
142
+ :auth => :writely, :header => {"If-Match" => "*"})
143
+ end
144
+
145
+ # Renames title of the spreadsheet.
146
+ def rename(title)
147
+ doc = @session.request(:get, self.document_feed_url, :auth => :writely)
148
+ edit_url = doc.css("link[rel='edit']").first["href"]
149
+ xml = <<-"EOS"
150
+ <atom:entry
151
+ xmlns:atom="http://www.w3.org/2005/Atom"
152
+ xmlns:docs="http://schemas.google.com/docs/2007">
153
+ <atom:category
154
+ scheme="http://schemas.google.com/g/2005#kind"
155
+ term="http://schemas.google.com/docs/2007#spreadsheet" label="spreadsheet"/>
156
+ <atom:title>#{h(title)}</atom:title>
157
+ </atom:entry>
158
+ EOS
159
+
160
+ @session.request(:put, edit_url, :data => xml, :auth => :writely)
161
+ end
162
+
163
+ alias title= rename
164
+
165
+ # Exports the spreadsheet in +format+ and returns it as String.
166
+ #
167
+ # +format+ can be either "xls", "csv", "pdf", "ods", "tsv" or "html".
168
+ # In format such as "csv", only the worksheet specified with +worksheet_index+ is
169
+ # exported.
170
+ def export_as_string(format, worksheet_index = nil)
171
+ gid_param = worksheet_index ? "&gid=#{worksheet_index}" : ""
172
+ url =
173
+ "https://spreadsheets.google.com/feeds/download/spreadsheets/Export" +
174
+ "?key=#{key}&exportFormat=#{format}#{gid_param}"
175
+ return @session.request(:get, url, :response_type => :raw)
176
+ end
177
+
178
+ # Exports the spreadsheet in +format+ as a local file.
179
+ #
180
+ # +format+ can be either "xls", "csv", "pdf", "ods", "tsv" or "html".
181
+ # If +format+ is nil, it is guessed from the file name.
182
+ # In format such as "csv", only the worksheet specified with +worksheet_index+ is exported.
183
+ #
184
+ # e.g.
185
+ # spreadsheet.export_as_file("hoge.ods")
186
+ # spreadsheet.export_as_file("hoge.csv", nil, 0)
187
+ def export_as_file(local_path, format = nil, worksheet_index = nil)
188
+ if !format
189
+ format = File.extname(local_path).gsub(/^\./, "")
190
+ if !SUPPORTED_EXPORT_FORMAT.include?(format)
191
+ raise(ArgumentError,
192
+ ("Cannot guess format from the file name: %s\n" +
193
+ "Specify format argument explicitly.") %
194
+ local_path)
195
+ end
196
+ end
197
+ open(local_path, "wb") do |f|
198
+ f.write(export_as_string(format, worksheet_index))
199
+ end
200
+ end
201
+
202
+ # Returns worksheets of the spreadsheet as array of GoogleSpreadsheet::Worksheet.
203
+ def worksheets
204
+ doc = @session.request(:get, @worksheets_feed_url)
205
+ if doc.root.name != "feed"
206
+ raise(GoogleSpreadsheet::Error,
207
+ "%s doesn't look like a worksheets feed URL because its root is not <feed>." %
208
+ @worksheets_feed_url)
209
+ end
210
+ result = []
211
+ doc.css("entry").each() do |entry|
212
+ title = entry.css("title").text
213
+ url = entry.css(
214
+ "link[rel='http://schemas.google.com/spreadsheets/2006#cellsfeed']")[0]["href"]
215
+ result.push(Worksheet.new(@session, self, url, title))
216
+ end
217
+ return result.freeze()
218
+ end
219
+
220
+ # Returns a GoogleSpreadsheet::Worksheet with the given title in the spreadsheet.
221
+ #
222
+ # Returns nil if not found. Returns the first one when multiple worksheets with the
223
+ # title are found.
224
+ def worksheet_by_title(title)
225
+ return self.worksheets.find(){ |ws| ws.title == title }
226
+ end
227
+
228
+ # Adds a new worksheet to the spreadsheet. Returns added GoogleSpreadsheet::Worksheet.
229
+ def add_worksheet(title, max_rows = 100, max_cols = 20)
230
+ xml = <<-"EOS"
231
+ <entry xmlns='http://www.w3.org/2005/Atom'
232
+ xmlns:gs='http://schemas.google.com/spreadsheets/2006'>
233
+ <title>#{h(title)}</title>
234
+ <gs:rowCount>#{h(max_rows)}</gs:rowCount>
235
+ <gs:colCount>#{h(max_cols)}</gs:colCount>
236
+ </entry>
237
+ EOS
238
+ doc = @session.request(:post, @worksheets_feed_url, :data => xml)
239
+ url = doc.css(
240
+ "link[rel='http://schemas.google.com/spreadsheets/2006#cellsfeed']")[0]["href"]
241
+ return Worksheet.new(@session, self, url, title)
242
+ end
243
+
244
+ # Returns GoogleSpreadsheet::Acl object for the spreadsheet.
245
+ #
246
+ # With the object, you can see and modify people who can access the spreadsheet.
247
+ # Modifications take effect immediately.
248
+ #
249
+ # Set params[:reload] to true to force reloading the title.
250
+ #
251
+ # e.g.
252
+ # # Dumps people who have access:
253
+ # for entry in spreadsheet.acl
254
+ # p [entry.scope_type, entry.scope, entry.role]
255
+ # # => e.g. ["user", "example1@gmail.com", "owner"]
256
+ # end
257
+ #
258
+ # # Shares the spreadsheet with new people:
259
+ # # NOTE: This sends email to the new people.
260
+ # spreadsheet.acl.push(
261
+ # {:scope_type => "user", :scope => "example2@gmail.com", :role => "reader"})
262
+ # spreadsheet.acl.push(
263
+ # {:scope_type => "user", :scope => "example3@gmail.com", :role => "writer"})
264
+ #
265
+ # # Changes the role of a person:
266
+ # spreadsheet.acl[1].role = "writer"
267
+ #
268
+ # # Deletes an ACL entry:
269
+ # spreadsheet.acl.delete(spreadsheet.acl[1])
270
+
271
+ def acl(params = {})
272
+ if !@acl || params[:reload]
273
+ @acl = Acl.new(@session, self.acl_feed_url)
274
+ end
275
+ return @acl
276
+ end
277
+
278
+ # DEPRECATED: Table and Record feeds are deprecated and they will not be available after
279
+ # March 2012.
280
+ #
281
+ # Returns list of tables in the spreadsheet.
282
+ def tables
283
+ warn(
284
+ "DEPRECATED: Google Spreadsheet Table and Record feeds are deprecated and they " +
285
+ "will not be available after March 2012.")
286
+ doc = @session.request(:get, self.tables_feed_url)
287
+ return doc.css("entry").map(){ |e| Table.new(@session, e) }.freeze()
288
+ end
289
+
290
+ def inspect
291
+ fields = {:worksheets_feed_url => self.worksheets_feed_url}
292
+ fields[:title] = @title if @title
293
+ return "\#<%p %s>" % [self.class, fields.map(){ |k, v| "%s=%p" % [k, v] }.join(", ")]
294
+ end
295
+
296
+ end
297
+
298
+ 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_spreadsheet/util"
5
+ require "google_spreadsheet/error"
6
+ require "google_spreadsheet/record"
7
+
8
+
9
+ module GoogleSpreadsheet
10
+
11
+ # DEPRECATED: Table and Record feeds are deprecated and they will not be available after
12
+ # March 2012.
13
+ #
14
+ # Use GoogleSpreadsheet::Worksheet#add_table to create table.
15
+ # Use GoogleSpreadsheet::Worksheet#tables to get GoogleSpreadsheet::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,30 @@
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 GoogleSpreadsheet
8
+
9
+ module Util #:nodoc:
10
+
11
+ module_function
12
+
13
+ def encode_query(params)
14
+ return params.map(){ |k, v| CGI.escape(k) + "=" + CGI.escape(v) }.join("&")
15
+ end
16
+
17
+ def concat_url(url, piece)
18
+ (url_base, url_query) = url.split(/\?/, 2)
19
+ (piece_base, piece_query) = piece.split(/\?/, 2)
20
+ result_query = [url_query, piece_query].select(){ |s| s && !s.empty? }.join("&")
21
+ return url_base + piece_base + (result_query.empty? ? "" : "?#{result_query}")
22
+ end
23
+
24
+ def h(str)
25
+ return CGI.escapeHTML(str.to_s())
26
+ end
27
+
28
+ end
29
+
30
+ 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_spreadsheet/util"
7
+ require "google_spreadsheet/error"
8
+ require "google_spreadsheet/table"
9
+ require "google_spreadsheet/list"
10
+
11
+
12
+ module GoogleSpreadsheet
13
+
14
+ # A worksheet (i.e. a tab) in a spreadsheet.
15
+ # Use GoogleSpreadsheet::Spreadsheet#worksheets to get GoogleSpreadsheet::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(GoogleSpreadsheet::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
+ # GoogleSpreadsheet::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(GoogleSpreadsheet::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(GoogleSpreadsheet::Error, "Update has failed: %s" %
292
+ interrupted["reason"])
293
+ end
294
+ if !(entry.css("batch|status").first["code"] =~ /^2/)
295
+ raise(GoogleSpreadsheet::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 GoogleSpreadsheet::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 GoogleSpreadsheet::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