google-cells 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +26 -0
  3. data/.ruby-gemset +1 -0
  4. data/.ruby-version +2 -0
  5. data/CHANGELOG.md +28 -0
  6. data/Gemfile +4 -0
  7. data/Gemfile.lock +73 -0
  8. data/LICENSE.txt +22 -0
  9. data/README.md +75 -0
  10. data/Rakefile +4 -0
  11. data/examples/oauth2_service_account.rb +17 -0
  12. data/examples/oauth2_web_flow.rb +66 -0
  13. data/examples/sinatra/routes.rb +21 -0
  14. data/google_cells.gemspec +30 -0
  15. data/lib/google_cells.rb +80 -0
  16. data/lib/google_cells/author.rb +7 -0
  17. data/lib/google_cells/cell.rb +32 -0
  18. data/lib/google_cells/cell_selector.rb +18 -0
  19. data/lib/google_cells/cell_selector/row_selector.rb +70 -0
  20. data/lib/google_cells/fetcher.rb +27 -0
  21. data/lib/google_cells/folder.rb +7 -0
  22. data/lib/google_cells/google_object.rb +31 -0
  23. data/lib/google_cells/reader.rb +19 -0
  24. data/lib/google_cells/row.rb +7 -0
  25. data/lib/google_cells/spreadsheet.rb +179 -0
  26. data/lib/google_cells/url_helper.rb +37 -0
  27. data/lib/google_cells/util.rb +17 -0
  28. data/lib/google_cells/version.rb +3 -0
  29. data/lib/google_cells/worksheet.rb +63 -0
  30. data/spec/google_cells/cell_selector/row_selector_spec.rb +104 -0
  31. data/spec/google_cells/cell_selector_spec.rb +26 -0
  32. data/spec/google_cells/fetcher_spec.rb +21 -0
  33. data/spec/google_cells/google_object_spec.rb +18 -0
  34. data/spec/google_cells/reader_spec.rb +24 -0
  35. data/spec/google_cells/spreadsheet_spec.rb +171 -0
  36. data/spec/google_cells/url_helper_spec.rb +15 -0
  37. data/spec/google_cells/worksheet_spec.rb +69 -0
  38. data/spec/google_cells_spec.rb +13 -0
  39. data/spec/spec_helper.rb +30 -0
  40. data/spec/vcr_cassettes/google_cells/cell_selector/each.yml +9147 -0
  41. data/spec/vcr_cassettes/google_cells/cell_selector/find_each.yml +10272 -0
  42. data/spec/vcr_cassettes/google_cells/cell_selector/find_each/selection.yml +947 -0
  43. data/spec/vcr_cassettes/google_cells/fetcher.yml +144 -0
  44. data/spec/vcr_cassettes/google_cells/reader.yml +144 -0
  45. data/spec/vcr_cassettes/google_cells/spreadsheet/cell_selector/worksheet.yml +276 -0
  46. data/spec/vcr_cassettes/google_cells/spreadsheet/copy.yml +447 -0
  47. data/spec/vcr_cassettes/google_cells/spreadsheet/copy/content.yml +18555 -0
  48. data/spec/vcr_cassettes/google_cells/spreadsheet/enfold.yml +259 -0
  49. data/spec/vcr_cassettes/google_cells/spreadsheet/folders.yml +319 -0
  50. data/spec/vcr_cassettes/google_cells/spreadsheet/get.yml +135 -0
  51. data/spec/vcr_cassettes/google_cells/spreadsheet/list.yml +276 -0
  52. data/spec/vcr_cassettes/google_cells/spreadsheet/worksheets.yml +276 -0
  53. data/spec/vcr_cassettes/google_cells/worksheet/save.yml +9555 -0
  54. data/spec/vcr_cassettes/google_cells/worksheets.yml +145 -0
  55. metadata +250 -0
@@ -0,0 +1,7 @@
1
+ module GoogleCells
2
+ class Author < GoogleCells::GoogleObject
3
+ @permanent_attributes = [ :name, :email ]
4
+ define_accessors
5
+ end
6
+ end
7
+
@@ -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,7 @@
1
+ module GoogleCells
2
+ class Folder < GoogleCells::GoogleObject
3
+ @permanent_attributes = [ :key, :spreadsheet ]
4
+ define_accessors
5
+ end
6
+ end
7
+
@@ -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,7 @@
1
+ module GoogleCells
2
+
3
+ class Row < GoogleCells::GoogleObject
4
+ @permanent_attributes = [:number, :cells, :worksheet]
5
+ define_accessors
6
+ end
7
+ end
@@ -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