google_drive 0.3.0
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.
- data/README.rdoc +89 -0
- data/doc_src/google_drive/acl.rb +20 -0
- data/doc_src/google_drive/acl_entry.rb +33 -0
- data/lib/google_drive.rb +120 -0
- data/lib/google_drive/acl.rb +124 -0
- data/lib/google_drive/acl_entry.rb +58 -0
- data/lib/google_drive/authentication_error.rb +14 -0
- data/lib/google_drive/client_login_fetcher.rb +56 -0
- data/lib/google_drive/collection.rb +54 -0
- data/lib/google_drive/error.rb +12 -0
- data/lib/google_drive/file.rb +217 -0
- data/lib/google_drive/list.rb +115 -0
- data/lib/google_drive/list_row.rb +84 -0
- data/lib/google_drive/oauth1_fetcher.rb +26 -0
- data/lib/google_drive/oauth2_fetcher.rb +47 -0
- data/lib/google_drive/record.rb +31 -0
- data/lib/google_drive/session.rb +436 -0
- data/lib/google_drive/spreadsheet.rb +220 -0
- data/lib/google_drive/table.rb +60 -0
- data/lib/google_drive/util.rb +55 -0
- data/lib/google_drive/worksheet.rb +445 -0
- metadata +121 -0
@@ -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
|