google-cells 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.
- 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
|