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,32 @@
|
|
1
|
+
module GoogleCells
|
2
|
+
class Cell < GoogleCells::GoogleObject
|
3
|
+
include Util
|
4
|
+
|
5
|
+
@permanent_attributes = [:title, :id, :value, :numeric_value, :row, :col,
|
6
|
+
:edit_url, :worksheet]
|
7
|
+
define_accessors
|
8
|
+
|
9
|
+
attr_reader :input_value
|
10
|
+
|
11
|
+
def input_value=(v)
|
12
|
+
@input_value = v
|
13
|
+
worksheet.track_changes(self)
|
14
|
+
v
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_xml
|
18
|
+
<<-EOS
|
19
|
+
<entry>
|
20
|
+
<batch:id>#{e(row)},#{e(col)}</batch:id>
|
21
|
+
<batch:operation type="update"/>
|
22
|
+
<id>#{e(id)}</id>
|
23
|
+
<link rel="edit" type="application/atom+xml"
|
24
|
+
href="#{e(edit_url)}"/>
|
25
|
+
<gs:cell row="#{e(row)}" col="#{e(col)}" inputValue="#{e(input_value)}"/>
|
26
|
+
</entry>
|
27
|
+
EOS
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/cell_selector/row_selector'
|
2
|
+
|
3
|
+
module GoogleCells
|
4
|
+
class CellSelector
|
5
|
+
include Reader
|
6
|
+
|
7
|
+
attr_accessor :min_row, :max_row, :min_col, :max_col, :worksheet
|
8
|
+
|
9
|
+
def initialize(ws)
|
10
|
+
@worksheet = ws
|
11
|
+
@min_row = 1
|
12
|
+
@max_row = worksheet.row_count
|
13
|
+
@min_col = 1
|
14
|
+
@max_col = worksheet.col_count
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module GoogleCells
|
2
|
+
class CellSelector
|
3
|
+
|
4
|
+
class RowSelector < CellSelector
|
5
|
+
|
6
|
+
DEFAULT_BATCH_SIZE = 10
|
7
|
+
|
8
|
+
def find_each(opts={}, &block)
|
9
|
+
size = (opts[:batch_size] || DEFAULT_BATCH_SIZE).to_i
|
10
|
+
rnum = @min_row
|
11
|
+
loop do
|
12
|
+
last = [rnum + size, @max_row].min
|
13
|
+
break if rnum > last
|
14
|
+
get_cells(rnum, last).each do |cells|
|
15
|
+
yield Row.new(cells:cells, number:rnum, worksheet:worksheet)
|
16
|
+
rnum += 1
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def each
|
22
|
+
all.each{|c| yield c}
|
23
|
+
end
|
24
|
+
|
25
|
+
def all
|
26
|
+
@rows = []
|
27
|
+
self.find_each(batch_size:@max_row - @min_row){|r| @rows << r}
|
28
|
+
@rows
|
29
|
+
end
|
30
|
+
|
31
|
+
def first
|
32
|
+
all.first
|
33
|
+
end
|
34
|
+
|
35
|
+
def from(num)
|
36
|
+
@min_row = num.to_i
|
37
|
+
self
|
38
|
+
end
|
39
|
+
|
40
|
+
def to(num)
|
41
|
+
@max_row = num.to_i
|
42
|
+
self
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def get_cells(start, last)
|
48
|
+
cells = []
|
49
|
+
each_entry(worksheet.cells_uri, 'return-empty' => 'true',
|
50
|
+
'min-row' => start.to_s, 'max-row' => last.to_s) do |entry|
|
51
|
+
gscell = entry.css("gs|cell")[0]
|
52
|
+
cell = Cell.new(
|
53
|
+
id: entry.css("id").text,
|
54
|
+
title: entry.css("title").text,
|
55
|
+
value: gscell.inner_text,
|
56
|
+
row: gscell["row"].to_i,
|
57
|
+
col: gscell["col"].to_i,
|
58
|
+
edit_url: entry.css("link[rel='edit']")[0]["href"],
|
59
|
+
input_value: gscell["inputValue"],
|
60
|
+
numeric_value: gscell["numericValue"],
|
61
|
+
worksheet: self.worksheet
|
62
|
+
)
|
63
|
+
cells[cell.row - start] ||= []
|
64
|
+
cells[cell.row - start][cell.col - 1] = cell
|
65
|
+
end
|
66
|
+
cells
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module GoogleCells
|
2
|
+
|
3
|
+
module Fetcher
|
4
|
+
|
5
|
+
BASE_URL = 'https://spreadsheets.google.com/feeds/spreadsheets/private/full'
|
6
|
+
|
7
|
+
def raw(url=nil, params={})
|
8
|
+
url ||= BASE_URL
|
9
|
+
res = request(:get, url, url_params: params)
|
10
|
+
res.body
|
11
|
+
end
|
12
|
+
|
13
|
+
def request(method, url, params={})
|
14
|
+
if params[:url_params] && !params[:url_params].empty?
|
15
|
+
url << '?' unless url[-1] == "?"
|
16
|
+
url << params[:url_params].to_a.map{|k,v| "#{k}=#{v}"}.join('&')
|
17
|
+
end
|
18
|
+
GoogleCells.client.authorization.fetch_access_token!
|
19
|
+
GoogleCells.client.execute!(
|
20
|
+
:http_method => method,
|
21
|
+
:uri => url,
|
22
|
+
:headers => params[:headers],
|
23
|
+
:body => params[:body]
|
24
|
+
)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module GoogleCells
|
2
|
+
|
3
|
+
class GoogleObject
|
4
|
+
class << self
|
5
|
+
attr_reader :permanent_attributes
|
6
|
+
|
7
|
+
def define_accessors
|
8
|
+
self.instance_eval do
|
9
|
+
@permanent_attributes.each do |k|
|
10
|
+
define_method(k){ @values[k] }
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def initialize(attribs={})
|
17
|
+
@values = {}
|
18
|
+
self.class.permanent_attributes.each{|a| @values[a] = attribs[a]}
|
19
|
+
|
20
|
+
extra = attribs.keys - self.class.permanent_attributes
|
21
|
+
extra.each do |a|
|
22
|
+
if self.respond_to?("#{a}=")
|
23
|
+
instance_variable_set("@#{a}".to_sym, attribs[a])
|
24
|
+
next
|
25
|
+
end
|
26
|
+
raise ArgumentError, "invalid attribute #{a} passed to #{
|
27
|
+
self.class}"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/fetcher'
|
2
|
+
|
3
|
+
module GoogleCells
|
4
|
+
|
5
|
+
module Reader
|
6
|
+
include GoogleCells::Fetcher
|
7
|
+
|
8
|
+
def each_entry(url=nil, params={}, &block)
|
9
|
+
doc = raw(url, params)
|
10
|
+
reader = Nokogiri::XML::Reader(doc)
|
11
|
+
reader.each do |node|
|
12
|
+
next unless node.name == 'entry' && node.node_type ==
|
13
|
+
Nokogiri::XML::Reader::TYPE_ELEMENT
|
14
|
+
block.call(Nokogiri.parse(node.outer_xml))
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
@@ -0,0 +1,179 @@
|
|
1
|
+
require 'google_cells/worksheet'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
module GoogleCells
|
5
|
+
|
6
|
+
class Spreadsheet < GoogleCells::GoogleObject
|
7
|
+
extend UrlHelper
|
8
|
+
extend Reader
|
9
|
+
|
10
|
+
@permanent_attributes = [ :title, :updated_at, :author, :key ]
|
11
|
+
define_accessors
|
12
|
+
|
13
|
+
class << self
|
14
|
+
|
15
|
+
def list
|
16
|
+
spreadsheets = []
|
17
|
+
each_entry do |entry|
|
18
|
+
args = parse_from_entry(entry)
|
19
|
+
spreadsheets << Spreadsheet.new(args)
|
20
|
+
end
|
21
|
+
spreadsheets
|
22
|
+
end
|
23
|
+
|
24
|
+
alias_method :all, :list
|
25
|
+
|
26
|
+
def get(key)
|
27
|
+
res = request(:get, worksheets_uri(key))
|
28
|
+
args = parse_from_entry(Nokogiri.parse(res.body), key)
|
29
|
+
Spreadsheet.new(args)
|
30
|
+
end
|
31
|
+
|
32
|
+
def copy(key, opts={})
|
33
|
+
params = {}
|
34
|
+
body = nil
|
35
|
+
{ :writers_can_share => 'writersCanShare',
|
36
|
+
:title => 'title' }.each do |sym,str|
|
37
|
+
next unless opts[sym]
|
38
|
+
body ||= {}
|
39
|
+
body[str] = opts.delete(sym)
|
40
|
+
end
|
41
|
+
if body
|
42
|
+
params[:body] = body.to_json
|
43
|
+
params[:headers] = {'Content-Type' => 'application/json'}
|
44
|
+
end
|
45
|
+
res = request(:post, copy_uri(key), params)
|
46
|
+
s = get(res.data['id'])
|
47
|
+
end
|
48
|
+
|
49
|
+
def share(key, params)
|
50
|
+
body = {}
|
51
|
+
[:role, :type, :value].each do |sym|
|
52
|
+
body[sym.to_s] = params.delete(sym)
|
53
|
+
end
|
54
|
+
params[:body] = body.to_json
|
55
|
+
|
56
|
+
params[:url_params] = {}
|
57
|
+
params[:url_params]['sendNotificationEmails'] = params.delete(
|
58
|
+
:send_notification_emails) if !params[:send_notification_emails].
|
59
|
+
to_s.empty?
|
60
|
+
params[:url_params]['emailMessage'] = params.delete(
|
61
|
+
:email_message) if params[:email_message]
|
62
|
+
|
63
|
+
params[:headers] = {'Content-Type' => 'application/json'}
|
64
|
+
|
65
|
+
res = request(:post, permissions_uri(key), params)
|
66
|
+
true
|
67
|
+
end
|
68
|
+
|
69
|
+
def delete(key)
|
70
|
+
request(:delete, file_uri(key))
|
71
|
+
end
|
72
|
+
|
73
|
+
def unsubscribe(params)
|
74
|
+
body = {}
|
75
|
+
body['id'] = params.delete(:id) if params[:id]
|
76
|
+
body['resourceId'] = params.delete(:resource_id) if params[:resource_id]
|
77
|
+
drive = GoogleCells.client.discovered_api('drive', 'v2')
|
78
|
+
GoogleCells.client.execute!(
|
79
|
+
:api_method => drive.channels.stop,
|
80
|
+
:body_object => body )
|
81
|
+
true
|
82
|
+
end
|
83
|
+
|
84
|
+
def subscribe(key, params)
|
85
|
+
body = {"type" => "web_hook"}
|
86
|
+
[:id, :address, :token, :expiration, :type].each do |sym|
|
87
|
+
body[sym.to_s] = params.delete(sym) if params[sym]
|
88
|
+
end
|
89
|
+
drive = GoogleCells.client.discovered_api('drive', 'v2')
|
90
|
+
res = GoogleCells.client.execute!(
|
91
|
+
:api_method => drive.files.watch,
|
92
|
+
:body_object => body,
|
93
|
+
:parameters => { 'fileId' => key })
|
94
|
+
res.data['resourceId']
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
%w( subscribe share ).each do |m|
|
99
|
+
define_method(m){|args| self.class.send(m, self.key, args) }
|
100
|
+
end
|
101
|
+
|
102
|
+
def unsubscribe(params)
|
103
|
+
self.class.unsubscribe(params)
|
104
|
+
end
|
105
|
+
|
106
|
+
def delete
|
107
|
+
self.class.delete(self.key)
|
108
|
+
end
|
109
|
+
|
110
|
+
def copy(opts={})
|
111
|
+
self.class.copy(self.key, opts)
|
112
|
+
end
|
113
|
+
|
114
|
+
def enfold(folder_key)
|
115
|
+
return true if @folders && @folders.select{|f| f.key == folder_key}.first
|
116
|
+
body = {'id' => self.key}.to_json
|
117
|
+
res = self.class.request(:post, self.class.folder_uri(folder_key),
|
118
|
+
:body => body, :headers => {'Content-Type' => 'application/json'})
|
119
|
+
@folders << Folder.new(spreadsheet:self, key:folder_key) if @folders
|
120
|
+
true
|
121
|
+
end
|
122
|
+
|
123
|
+
def defold(folder_key)
|
124
|
+
klass = self.class
|
125
|
+
res = klass.request(:delete, klass.child_uri(folder_key, self.key))
|
126
|
+
@folders = nil
|
127
|
+
true
|
128
|
+
end
|
129
|
+
|
130
|
+
def folders
|
131
|
+
return @folders if @folders
|
132
|
+
res = self.class.request(:get, self.class.file_uri(key))
|
133
|
+
data = JSON.parse(res.body)
|
134
|
+
return @folders = [] if data['parents'].nil?
|
135
|
+
@folders = data['parents'].map do |f|
|
136
|
+
Folder.new(spreadsheet: self, key:f['id'])
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def worksheets
|
141
|
+
return @worksheets if @worksheets
|
142
|
+
@worksheets = []
|
143
|
+
self.class.each_entry(worksheets_uri) do |entry|
|
144
|
+
args = {
|
145
|
+
title: entry.css("title").text,
|
146
|
+
updated_at: entry.css("updated").text,
|
147
|
+
cells_uri: entry.css(
|
148
|
+
"link[rel='http://schemas.google.com/spreadsheets/2006#cellsfeed']"
|
149
|
+
)[0]["href"],
|
150
|
+
lists_uri: entry.css(
|
151
|
+
"link[rel='http://schemas.google.com/spreadsheets/2006#listfeed']"
|
152
|
+
)[0]["href"],
|
153
|
+
row_count: entry.css("gs|rowCount").text.to_i,
|
154
|
+
col_count: entry.css("gs|colCount").text.to_i,
|
155
|
+
spreadsheet: self
|
156
|
+
}
|
157
|
+
@worksheets << Worksheet.new(args)
|
158
|
+
end
|
159
|
+
return @worksheets
|
160
|
+
end
|
161
|
+
|
162
|
+
private
|
163
|
+
|
164
|
+
def self.parse_from_entry(entry, key=nil)
|
165
|
+
key ||= entry.css("link").select{|el| el['rel'] == 'alternate'}.
|
166
|
+
first['href'][/key=.+/][4..-1]
|
167
|
+
{ title: entry.css("title").first.text,
|
168
|
+
key: key,
|
169
|
+
updated_at: entry.css("updated").first.text,
|
170
|
+
author: Author.new(
|
171
|
+
name: entry.css("author/name").first.text,
|
172
|
+
email: entry.css("author/email").first.text
|
173
|
+
)
|
174
|
+
}
|
175
|
+
end
|
176
|
+
|
177
|
+
def worksheets_uri; self.class.worksheets_uri(key); end
|
178
|
+
end
|
179
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module GoogleCells
|
2
|
+
|
3
|
+
module UrlHelper
|
4
|
+
|
5
|
+
def worksheets_uri(key)
|
6
|
+
"https://spreadsheets.google.com/feeds/worksheets/#{key}/private/full"
|
7
|
+
end
|
8
|
+
|
9
|
+
def copy_uri(key)
|
10
|
+
"https://www.googleapis.com/drive/v2/files/#{key}/copy"
|
11
|
+
end
|
12
|
+
|
13
|
+
def permissions_uri(key)
|
14
|
+
"https://www.googleapis.com/drive/v2/files/#{key}/permissions"
|
15
|
+
end
|
16
|
+
|
17
|
+
def folder_uri(key)
|
18
|
+
"https://www.googleapis.com/drive/v2/files/#{key}/children"
|
19
|
+
end
|
20
|
+
|
21
|
+
def child_uri(folder_key, child_key)
|
22
|
+
"https://www.googleapis.com/drive/v2/files/#{folder_key}/children/#{child_key}"
|
23
|
+
end
|
24
|
+
|
25
|
+
def file_uri(key)
|
26
|
+
"https://www.googleapis.com/drive/v2/files/#{key}"
|
27
|
+
end
|
28
|
+
|
29
|
+
def watch_uri(key)
|
30
|
+
"https://www.googleapis.com/drive/v2/files/#{key}/watch"
|
31
|
+
end
|
32
|
+
|
33
|
+
def unwatch_uri
|
34
|
+
"https://www.googleapis.com/drive/v2/channels/stop"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|