google-cells 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +26 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +2 -0
- data/CHANGELOG.md +28 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +73 -0
- data/LICENSE.txt +22 -0
- data/README.md +75 -0
- data/Rakefile +4 -0
- data/examples/oauth2_service_account.rb +17 -0
- data/examples/oauth2_web_flow.rb +66 -0
- data/examples/sinatra/routes.rb +21 -0
- data/google_cells.gemspec +30 -0
- data/lib/google_cells.rb +80 -0
- data/lib/google_cells/author.rb +7 -0
- data/lib/google_cells/cell.rb +32 -0
- data/lib/google_cells/cell_selector.rb +18 -0
- data/lib/google_cells/cell_selector/row_selector.rb +70 -0
- data/lib/google_cells/fetcher.rb +27 -0
- data/lib/google_cells/folder.rb +7 -0
- data/lib/google_cells/google_object.rb +31 -0
- data/lib/google_cells/reader.rb +19 -0
- data/lib/google_cells/row.rb +7 -0
- data/lib/google_cells/spreadsheet.rb +179 -0
- data/lib/google_cells/url_helper.rb +37 -0
- data/lib/google_cells/util.rb +17 -0
- data/lib/google_cells/version.rb +3 -0
- data/lib/google_cells/worksheet.rb +63 -0
- data/spec/google_cells/cell_selector/row_selector_spec.rb +104 -0
- data/spec/google_cells/cell_selector_spec.rb +26 -0
- data/spec/google_cells/fetcher_spec.rb +21 -0
- data/spec/google_cells/google_object_spec.rb +18 -0
- data/spec/google_cells/reader_spec.rb +24 -0
- data/spec/google_cells/spreadsheet_spec.rb +171 -0
- data/spec/google_cells/url_helper_spec.rb +15 -0
- data/spec/google_cells/worksheet_spec.rb +69 -0
- data/spec/google_cells_spec.rb +13 -0
- data/spec/spec_helper.rb +30 -0
- data/spec/vcr_cassettes/google_cells/cell_selector/each.yml +9147 -0
- data/spec/vcr_cassettes/google_cells/cell_selector/find_each.yml +10272 -0
- data/spec/vcr_cassettes/google_cells/cell_selector/find_each/selection.yml +947 -0
- data/spec/vcr_cassettes/google_cells/fetcher.yml +144 -0
- data/spec/vcr_cassettes/google_cells/reader.yml +144 -0
- data/spec/vcr_cassettes/google_cells/spreadsheet/cell_selector/worksheet.yml +276 -0
- data/spec/vcr_cassettes/google_cells/spreadsheet/copy.yml +447 -0
- data/spec/vcr_cassettes/google_cells/spreadsheet/copy/content.yml +18555 -0
- data/spec/vcr_cassettes/google_cells/spreadsheet/enfold.yml +259 -0
- data/spec/vcr_cassettes/google_cells/spreadsheet/folders.yml +319 -0
- data/spec/vcr_cassettes/google_cells/spreadsheet/get.yml +135 -0
- data/spec/vcr_cassettes/google_cells/spreadsheet/list.yml +276 -0
- data/spec/vcr_cassettes/google_cells/spreadsheet/worksheets.yml +276 -0
- data/spec/vcr_cassettes/google_cells/worksheet/save.yml +9555 -0
- data/spec/vcr_cassettes/google_cells/worksheets.yml +145 -0
- metadata +250 -0
@@ -0,0 +1,17 @@
|
|
1
|
+
module GoogleCells
|
2
|
+
module Util
|
3
|
+
|
4
|
+
def e(str)
|
5
|
+
CGI.escapeHTML(str.to_s()).gsub(/\n/, '
')
|
6
|
+
end
|
7
|
+
|
8
|
+
def concat_url(url, piece)
|
9
|
+
(url_base, url_query) = url.split(/\?/, 2)
|
10
|
+
(piece_base, piece_query) = piece.split(/\?/, 2)
|
11
|
+
result_query = [url_query, piece_query].select(){ |s| s && !s.empty? }.join("&")
|
12
|
+
return (url_base || "") +
|
13
|
+
(piece_base || "") +
|
14
|
+
(result_query.empty? ? "" : "?#{result_query}")
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'cgi'
|
2
|
+
|
3
|
+
module GoogleCells
|
4
|
+
class Worksheet < GoogleCells::GoogleObject
|
5
|
+
include Fetcher
|
6
|
+
include Util
|
7
|
+
|
8
|
+
@permanent_attributes = [ :etag, :title, :updated_at, :cells_uri,
|
9
|
+
:lists_uri, :spreadsheet, :row_count, :col_count ]
|
10
|
+
define_accessors
|
11
|
+
|
12
|
+
def rows
|
13
|
+
GoogleCells::CellSelector::RowSelector.new(self)
|
14
|
+
end
|
15
|
+
|
16
|
+
class UpdateError < StandardError ; end
|
17
|
+
|
18
|
+
def save!
|
19
|
+
return if @changed_cells.nil? || @changed_cells.empty?
|
20
|
+
batch_url = concat_url(cells_uri, "/batch")
|
21
|
+
response = request(:post, batch_url, body: to_xml, headers:{
|
22
|
+
"Content-Type" => "application/atom+xml", "If-Match" => "*"})
|
23
|
+
doc = Nokogiri.parse(response.body)
|
24
|
+
|
25
|
+
for entry in doc.css("atom|entry")
|
26
|
+
interrupted = entry.css("batch|interrupted")[0]
|
27
|
+
if interrupted
|
28
|
+
raise(UpdateError, "Update failed: %s" % interrupted["reason"])
|
29
|
+
end
|
30
|
+
if !(entry.css("batch|status").first["code"] =~ /^2/)
|
31
|
+
raise(UpdateError, "Update failed for cell %s: %s" %
|
32
|
+
[entry.css("atom|id").text, entry.css("batch|status")[0]["reason"]])
|
33
|
+
end
|
34
|
+
end
|
35
|
+
@changed_cells = {}
|
36
|
+
true
|
37
|
+
end
|
38
|
+
|
39
|
+
def track_changes(cell)
|
40
|
+
@changed_cells ||= {}
|
41
|
+
@changed_cells[cell.title] = cell # track only most recent change
|
42
|
+
nil
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def to_xml
|
48
|
+
xml = <<-EOS
|
49
|
+
<feed xmlns="http://www.w3.org/2005/Atom"
|
50
|
+
xmlns:batch="http://schemas.google.com/gdata/batch"
|
51
|
+
xmlns:gs="http://schemas.google.com/spreadsheets/2006">
|
52
|
+
<id>#{e(self.cells_uri)}</id>
|
53
|
+
EOS
|
54
|
+
@changed_cells.each do |title, cell|
|
55
|
+
xml << cell.to_xml
|
56
|
+
end
|
57
|
+
xml << <<-"EOS"
|
58
|
+
</feed>
|
59
|
+
EOS
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
@@ -0,0 +1,104 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe GoogleCells::CellSelector::RowSelector do
|
4
|
+
|
5
|
+
let(:subject) do
|
6
|
+
w = nil
|
7
|
+
VCR.use_cassette('google_cells/spreadsheet/cell_selector/worksheet') do
|
8
|
+
s = GoogleCells::Spreadsheet.list.first
|
9
|
+
w = s.worksheets.first
|
10
|
+
end
|
11
|
+
GoogleCells::CellSelector::RowSelector.new(w)
|
12
|
+
end
|
13
|
+
|
14
|
+
describe "#find_each" do
|
15
|
+
|
16
|
+
it "iterates over all the rows" do
|
17
|
+
count = 0
|
18
|
+
|
19
|
+
VCR.use_cassette('google_cells/cell_selector/find_each') do
|
20
|
+
subject.find_each do |r|
|
21
|
+
count += 1
|
22
|
+
r.class.should eq GoogleCells::Row
|
23
|
+
end
|
24
|
+
end
|
25
|
+
count.should eq 100
|
26
|
+
end
|
27
|
+
|
28
|
+
it "iterates over rows specified in cell selector" do
|
29
|
+
count = 0
|
30
|
+
|
31
|
+
VCR.use_cassette('google_cells/cell_selector/find_each/selection') do
|
32
|
+
s = GoogleCells::Spreadsheet.list.first
|
33
|
+
w = s.worksheets.first
|
34
|
+
rs = subject.from(5).to(10)
|
35
|
+
rs.find_each do |r|
|
36
|
+
count += 1
|
37
|
+
r.class.should eq GoogleCells::Row
|
38
|
+
end
|
39
|
+
end
|
40
|
+
count.should eq 6
|
41
|
+
end
|
42
|
+
|
43
|
+
context "optional batch sizes" do
|
44
|
+
it "fetches in batches 10" do
|
45
|
+
batch = 10
|
46
|
+
subject.should_receive(:get_cells).exactly(10).times.and_return((1..batch).to_a)
|
47
|
+
subject.find_each(batch_size:batch){|r| r}
|
48
|
+
end
|
49
|
+
it "fetches in batches 3" do
|
50
|
+
batch = 3
|
51
|
+
subject.should_receive(:get_cells).exactly(34).times.and_return((1..batch).to_a)
|
52
|
+
subject.find_each(batch_size:batch){|r| r}
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
describe "#each" do
|
58
|
+
|
59
|
+
it "iterates over rows specified in cell selector" do
|
60
|
+
count = 0
|
61
|
+
|
62
|
+
VCR.use_cassette('google_cells/cell_selector/each') do
|
63
|
+
subject.each do |r|
|
64
|
+
count += 1
|
65
|
+
r.class.should eq GoogleCells::Row
|
66
|
+
end
|
67
|
+
end
|
68
|
+
count.should eq 100
|
69
|
+
end
|
70
|
+
|
71
|
+
it "fetches in a single request" do
|
72
|
+
subject.should_receive(:get_cells).and_return((1..subject.worksheet.
|
73
|
+
row_count).to_a)
|
74
|
+
subject.each{|r| r}
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
describe "#from" do
|
79
|
+
it "sets its min row" do
|
80
|
+
subject.from(5)
|
81
|
+
subject.min_row.should eq 5
|
82
|
+
subject.from(10)
|
83
|
+
subject.min_row.should eq 10
|
84
|
+
end
|
85
|
+
|
86
|
+
it "returns itself" do
|
87
|
+
subject.from(subject.min_row).should eq subject
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
describe "#to" do
|
92
|
+
it "sets its max row" do
|
93
|
+
subject.to(5)
|
94
|
+
subject.max_row.should eq 5
|
95
|
+
subject.to(10)
|
96
|
+
subject.max_row.should eq 10
|
97
|
+
end
|
98
|
+
|
99
|
+
it "returns itself" do
|
100
|
+
subject.from(subject.max_row).should eq subject
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe GoogleCells::CellSelector do
|
4
|
+
|
5
|
+
let(:subject) do
|
6
|
+
w = nil
|
7
|
+
VCR.use_cassette('google_cells/spreadsheet/list') do
|
8
|
+
s = GoogleCells::Spreadsheet.list.first
|
9
|
+
w = s.worksheets.first
|
10
|
+
end
|
11
|
+
GoogleCells::CellSelector.new(w)
|
12
|
+
end
|
13
|
+
|
14
|
+
it { should respond_to(:min_row) }
|
15
|
+
it { should respond_to(:max_row) }
|
16
|
+
it { should respond_to(:min_col) }
|
17
|
+
it { should respond_to(:max_col) }
|
18
|
+
it { should respond_to(:worksheet) }
|
19
|
+
|
20
|
+
context "default values should be derived from worksheet" do
|
21
|
+
its(:min_row){ should eq 1 }
|
22
|
+
its(:max_row){ should eq 100 }
|
23
|
+
its(:min_col){ should eq 1 }
|
24
|
+
its(:max_col){ should eq 18 }
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe GoogleCells::Fetcher do
|
4
|
+
|
5
|
+
let(:object) do
|
6
|
+
obj = Object.new
|
7
|
+
obj.extend(GoogleCells::Fetcher)
|
8
|
+
obj
|
9
|
+
end
|
10
|
+
|
11
|
+
describe "#raw" do
|
12
|
+
|
13
|
+
it "returns the raw xml body of an http GET response" do
|
14
|
+
VCR.use_cassette('google_cells/fetcher') do |c|
|
15
|
+
doc = object.raw
|
16
|
+
doc.should_not be_empty
|
17
|
+
Nokogiri::XML(doc).errors.should be_empty
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe GoogleCells::GoogleObject do
|
4
|
+
|
5
|
+
context "subclass declaration" do
|
6
|
+
|
7
|
+
class TestObject < GoogleCells::GoogleObject
|
8
|
+
@permanent_attributes = %w{ name email }
|
9
|
+
define_accessors
|
10
|
+
end
|
11
|
+
|
12
|
+
it "automatically generates accessors for perm attribs" do
|
13
|
+
o = TestObject.new
|
14
|
+
o.should respond_to :name
|
15
|
+
o.should respond_to :email
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe GoogleCells::Reader do
|
4
|
+
|
5
|
+
let(:object) do
|
6
|
+
obj = Object.new
|
7
|
+
obj.extend(GoogleCells::Reader)
|
8
|
+
obj
|
9
|
+
end
|
10
|
+
|
11
|
+
describe "#each_entry" do
|
12
|
+
|
13
|
+
it "iterates over raw xml entries" do
|
14
|
+
VCR.use_cassette('google_cells/reader') do |c|
|
15
|
+
count = 0
|
16
|
+
object.each_entry do |e|
|
17
|
+
e.class.should eq Nokogiri::XML::Document
|
18
|
+
count +=1
|
19
|
+
end
|
20
|
+
count.should eq 3
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,171 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe GoogleCells::Spreadsheet do
|
4
|
+
|
5
|
+
let(:klass){GoogleCells::Spreadsheet}
|
6
|
+
|
7
|
+
it { should respond_to(:title) }
|
8
|
+
it { should respond_to(:key) }
|
9
|
+
it { should respond_to(:updated_at) }
|
10
|
+
it { should respond_to(:author) }
|
11
|
+
|
12
|
+
describe ".share" do
|
13
|
+
|
14
|
+
let(:body){{role:'owner',type:'user',value:'me@me.com'}}
|
15
|
+
|
16
|
+
it "creates a new permission for the passed args" do
|
17
|
+
klass.should_receive(:request).with(:post, "https://www.googleapis.com" +
|
18
|
+
"/drive/v2/files/0AsfWTr-e4bf1dFVFRWtuSFJWTm1XeWhRdUt2MWwtQnc/"+
|
19
|
+
"permissions", {:body=>"{\"role\":\"" + "owner\",\"type\":\"user\"" +
|
20
|
+
",\"value\":\"me@me.com\"}", :url_params=>{},
|
21
|
+
:headers=>{"Content-Type"=>"application/json"}})
|
22
|
+
klass.share('0AsfWTr-e4bf1dFVFRWtuSFJWTm1XeWhRdUt2MWwtQnc', body)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
describe ".copy" do
|
27
|
+
|
28
|
+
before(:each) do
|
29
|
+
@s, @c = nil
|
30
|
+
VCR.use_cassette('google_cells/spreadsheet/copy') do
|
31
|
+
@s = GoogleCells::Spreadsheet.list.first
|
32
|
+
@c = klass.copy(s.key)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
let(:s){ @s }
|
36
|
+
|
37
|
+
context "new copy" do
|
38
|
+
subject{ @c }
|
39
|
+
|
40
|
+
its(:key){ should_not eq s.key }
|
41
|
+
its(:title){ should eq s.title }
|
42
|
+
|
43
|
+
it "should have same content" do
|
44
|
+
VCR.use_cassette('google_cells/spreadsheet/copy/content') do
|
45
|
+
svals = s.worksheets[0].rows.first.cells.map(&:value)
|
46
|
+
cvals = @c.worksheets[0].rows.first.cells.map(&:value)
|
47
|
+
svals.should eq cvals
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
describe ".list" do
|
54
|
+
|
55
|
+
before(:each) do
|
56
|
+
@list = []
|
57
|
+
VCR.use_cassette('google_cells/spreadsheet/list') do |c|
|
58
|
+
@list = klass.list
|
59
|
+
end
|
60
|
+
end
|
61
|
+
let(:list){ @list }
|
62
|
+
|
63
|
+
it "returns a list" do
|
64
|
+
list.count.should eq 3
|
65
|
+
end
|
66
|
+
|
67
|
+
it "returns spreadsheet objects" do
|
68
|
+
list.each{|s| s.class.should eq subject.class }
|
69
|
+
end
|
70
|
+
|
71
|
+
context "loading new spreadsheets correcty" do
|
72
|
+
subject {list.first}
|
73
|
+
|
74
|
+
its(:title){ should eq "My Spreadsheet" }
|
75
|
+
its(:updated_at){ should eq '2014-02-23T04:52:51.908Z' }
|
76
|
+
its(:key){ should eq '0ApTxW-6l0Ch_dHFyNHNwX0NjTHVIVk9ZS2duQ2ptUlE' }
|
77
|
+
|
78
|
+
context "author" do
|
79
|
+
subject {list.first.author}
|
80
|
+
|
81
|
+
its(:name){ should eq '194578754295' }
|
82
|
+
its(:email){ should eq '194578754295@developer.gserviceaccount.com' }
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
describe ".get" do
|
88
|
+
|
89
|
+
before(:each) do
|
90
|
+
@s = nil
|
91
|
+
VCR.use_cassette('google_cells/spreadsheet/get') do
|
92
|
+
@s = klass.get('0AsfWTr-e4bf1dFVFRWtuSFJWTm1XeWhRdUt2MWwtQnc')
|
93
|
+
end
|
94
|
+
end
|
95
|
+
subject{ @s }
|
96
|
+
|
97
|
+
its(:class){ should eq klass }
|
98
|
+
its(:title){ should eq 'My Spreadsheet' }
|
99
|
+
its(:updated_at){ should eq '2014-02-23T02:25:18.152Z' }
|
100
|
+
|
101
|
+
context "author" do
|
102
|
+
subject{ @s.author }
|
103
|
+
its(:name){ should eq '194578754295' }
|
104
|
+
# its(:email){ should eq '194578754295@developer.gserviceaccount.com' }
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
describe "#enfold" do
|
109
|
+
|
110
|
+
before(:each) do
|
111
|
+
@s = nil
|
112
|
+
VCR.use_cassette('google_cells/spreadsheet/enfold') do
|
113
|
+
@s = GoogleCells::Spreadsheet.list.first
|
114
|
+
@s.instance_variable_set(:@folders, [])
|
115
|
+
@s.enfold('0B5TxW-6l0Ch_elJwQlVMaGcwTjA')
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
context "folders" do
|
120
|
+
subject{ @s.instance_variable_get(:@folders) }
|
121
|
+
its(:count){ should eq 1 }
|
122
|
+
|
123
|
+
it "sets key" do
|
124
|
+
subject.first.key.should eq '0B5TxW-6l0Ch_elJwQlVMaGcwTjA'
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
describe "#folders" do
|
130
|
+
|
131
|
+
before(:each) do
|
132
|
+
@s = nil
|
133
|
+
VCR.use_cassette('google_cells/spreadsheet/folders') do
|
134
|
+
@s = GoogleCells::Spreadsheet.list.first
|
135
|
+
@s.folders
|
136
|
+
end
|
137
|
+
end
|
138
|
+
it{ @s.folders.count.should eq 1 }
|
139
|
+
|
140
|
+
context "folders" do
|
141
|
+
subject{ @s.folders.first }
|
142
|
+
|
143
|
+
its(:class){ should eq GoogleCells::Folder }
|
144
|
+
its(:key){ should eq '0AJTxW-6l0Ch_Uk9PVA' }
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
describe "#worksheets" do
|
149
|
+
|
150
|
+
before(:each) do
|
151
|
+
@s = nil
|
152
|
+
VCR.use_cassette('google_cells/spreadsheet/worksheets') do
|
153
|
+
@s = klass.list.first
|
154
|
+
@s.worksheets.count.should eq 1
|
155
|
+
end
|
156
|
+
end
|
157
|
+
context "worksheets" do
|
158
|
+
subject{ @s.worksheets.first }
|
159
|
+
|
160
|
+
its(:title){ should eq 'Sheet1' }
|
161
|
+
its(:updated_at){ should eq '2014-02-23T02:25:18.152Z' }
|
162
|
+
its(:cells_uri){ should eq 'https://spreadsheets.google.com/feeds/'+
|
163
|
+
'cells/0ApTxW-6l0Ch_dHFyNHNwX0NjTHVIVk9ZS2duQ2ptUlE/od6/private/full' }
|
164
|
+
its(:lists_uri){ should eq 'https://spreadsheets.google.com/feeds/'+
|
165
|
+
'list/0ApTxW-6l0Ch_dHFyNHNwX0NjTHVIVk9ZS2duQ2ptUlE/od6/private/full' }
|
166
|
+
its(:row_count){ should eq 100 }
|
167
|
+
its(:col_count){ should eq 18 }
|
168
|
+
its(:spreadsheet){ should eq @s }
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|