googletastic 0.0.4 → 0.0.5
Sign up to get free protection for your applications and to get access to all the features.
- data/Rakefile +7 -2
- data/lib/googletastic.rb +1 -1
- data/lib/googletastic/form.rb +203 -107
- data/rails/init.rb +1 -0
- data/spec/googletastic/form_spec.rb +12 -5
- metadata +4 -3
data/Rakefile
CHANGED
@@ -31,7 +31,7 @@ spec = Gem::Specification.new do |s|
|
|
31
31
|
s.rubyforge_project = "googletastic"
|
32
32
|
s.platform = Gem::Platform::RUBY
|
33
33
|
s.files = %w(README.textile Rakefile) +
|
34
|
-
Dir["{googletastic,lib,spec}/**/*"] -
|
34
|
+
Dir["{googletastic,lib,spec,rails}/**/*"] -
|
35
35
|
Dir["spec/tmp"]
|
36
36
|
s.extra_rdoc_files = %w(README.textile)
|
37
37
|
s.require_path = "lib"
|
@@ -68,7 +68,12 @@ end
|
|
68
68
|
|
69
69
|
desc "Install the gem locally"
|
70
70
|
task :install => [:package] do
|
71
|
-
sh %{sudo gem install pkg
|
71
|
+
sh %{sudo gem install pkg/#{spec.name}-#{spec.version} --no-ri --no-rdoc}
|
72
|
+
end
|
73
|
+
|
74
|
+
desc "Publish gem to rubygems"
|
75
|
+
task :publish => [:package] do
|
76
|
+
%x[gem push pkg/#{spec.name}-#{spec.version}.gem]
|
72
77
|
end
|
73
78
|
|
74
79
|
desc "Run all specs"
|
data/lib/googletastic.rb
CHANGED
data/lib/googletastic/form.rb
CHANGED
@@ -3,11 +3,17 @@ class Googletastic::Form < Googletastic::Base
|
|
3
3
|
|
4
4
|
FORM_KEY_EXPRESSION = /formkey["|']\s*:["|']\s*([^"|']+)"/ unless defined?(FORM_KEY_EXPRESSION)
|
5
5
|
|
6
|
-
attr_accessor :title, :body, :
|
6
|
+
attr_accessor :title, :body, :form_key, :description, :fields, :properties
|
7
|
+
attr_accessor :authenticity_token, :spreadsheet, :form_only
|
7
8
|
|
8
|
-
|
9
|
-
|
10
|
-
@
|
9
|
+
# uses mechanize to hackily find form key
|
10
|
+
def form_key
|
11
|
+
unless @form_key
|
12
|
+
puts "Getting form key for '#{self.title}'"
|
13
|
+
@form_key = self.class.find_form_key(self.id)
|
14
|
+
end
|
15
|
+
|
16
|
+
@form_key
|
11
17
|
end
|
12
18
|
|
13
19
|
def submit_url
|
@@ -18,18 +24,25 @@ class Googletastic::Form < Googletastic::Base
|
|
18
24
|
self.class.show_url(self.form_key)
|
19
25
|
end
|
20
26
|
|
21
|
-
def
|
22
|
-
|
27
|
+
def from_google_tag(key)
|
28
|
+
self.class.from_google_tag(key)
|
23
29
|
end
|
24
30
|
|
25
|
-
def
|
26
|
-
|
27
|
-
@properties
|
31
|
+
def to_google_tag(key)
|
32
|
+
self.class.to_google_tag(key)
|
28
33
|
end
|
29
34
|
|
30
35
|
# this you want to call JUST BEFORE you render in the view
|
31
36
|
# body still gives you the nokogiri element
|
32
37
|
def render
|
38
|
+
add_redirect(body, options, &block)
|
39
|
+
|
40
|
+
body.xpath("//textarea").each do |node|
|
41
|
+
node.add_child Nokogiri::XML::Text.new("\n", body)
|
42
|
+
node.remove_attribute("rows")
|
43
|
+
node.remove_attribute("cols")
|
44
|
+
end
|
45
|
+
|
33
46
|
if self.form_only
|
34
47
|
result = body.xpath("//form").first.unlink
|
35
48
|
else
|
@@ -101,6 +114,25 @@ class Googletastic::Form < Googletastic::Base
|
|
101
114
|
"http://spreadsheets.google.com/viewform?formkey=#{id}"
|
102
115
|
end
|
103
116
|
|
117
|
+
def tag_map
|
118
|
+
@tag_map ||= {
|
119
|
+
"text" => "text",
|
120
|
+
"paragraph-text" => "textarea",
|
121
|
+
"grid" => "grid",
|
122
|
+
"scale" => "range",
|
123
|
+
"radio" => "radio",
|
124
|
+
"checkbox" => "checkbox"
|
125
|
+
}
|
126
|
+
end
|
127
|
+
|
128
|
+
def from_google_tag(key)
|
129
|
+
tag_map[key]
|
130
|
+
end
|
131
|
+
|
132
|
+
def to_google_tag(key)
|
133
|
+
tag_map[tag_map.values.detect {|i| i == key.to_s}]
|
134
|
+
end
|
135
|
+
|
104
136
|
def unmarshall(xml)
|
105
137
|
records = xml.xpath("//atom:entry", ns_tag("atom")).collect do |record|
|
106
138
|
#id = record.xpath("atom:id", ns_tag("atom")).first.text.gsub("http://spreadsheets.google.com/feeds/spreadsheets/", "")
|
@@ -111,131 +143,195 @@ class Googletastic::Form < Googletastic::Base
|
|
111
143
|
title = record.xpath("atom:title", ns_tag("atom")).first.text
|
112
144
|
created_at = record.xpath("atom:published", ns_tag("atom")).text
|
113
145
|
updated_at = record.xpath("atom:updated", ns_tag("atom")).text
|
114
|
-
|
146
|
+
|
115
147
|
# same as spreadsheet
|
116
148
|
Googletastic::Form.new(
|
117
|
-
:id
|
118
|
-
:title
|
149
|
+
:id => id,
|
150
|
+
:title => title,
|
119
151
|
:updated_at => DateTime.parse(updated_at)
|
120
152
|
)
|
121
153
|
end
|
122
154
|
records
|
123
155
|
end
|
156
|
+
|
157
|
+
def agent(force = false)
|
158
|
+
if force || !@agent
|
159
|
+
@agent = defined?(Mechanize) ? Mechanize.new : WWW::Mechanize.new
|
160
|
+
# google wants recent browsers!
|
161
|
+
# http://docs.google.com/support/bin/answer.py?answer=37560&hl=en
|
162
|
+
@agent.user_agent = "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_2; ru-ru) AppleWebKit/533.2+ (KHTML, like Gecko) Version/4.0.4 Safari/531.21.10"
|
163
|
+
login_form = @agent.get(mechanize_url(id)).forms.first
|
164
|
+
login_form.Email = Googletastic.credentials[:username].split("@").first # don't want emails
|
165
|
+
login_form.Passwd = Googletastic.credentials[:password]
|
166
|
+
@agent.submit(login_form)
|
167
|
+
end
|
168
|
+
|
169
|
+
@agent
|
170
|
+
end
|
124
171
|
|
125
|
-
|
126
|
-
|
127
|
-
def get_form_key
|
128
|
-
begin
|
129
|
-
agent = defined?(Mechanize) ? Mechanize.new : WWW::Mechanize.new
|
130
|
-
# google wants recent browsers!
|
131
|
-
# http://docs.google.com/support/bin/answer.py?answer=37560&hl=en
|
132
|
-
agent.user_agent = "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_2; ru-ru) AppleWebKit/533.2+ (KHTML, like Gecko) Version/4.0.4 Safari/531.21.10"
|
172
|
+
def mechanize_url(id)
|
133
173
|
url = "http://spreadsheets.google.com/"
|
134
174
|
# for spreadsheet, we need the domain!
|
135
175
|
if Googletastic.credentials[:domain]
|
136
176
|
url << "a/#{Googletastic.credentials[:domain]}/"
|
137
177
|
end
|
138
|
-
url << "ccc?key=#{
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
178
|
+
url << "ccc?key=#{id}&hl=en&pli=1"
|
179
|
+
url
|
180
|
+
end
|
181
|
+
|
182
|
+
# optimize this to login only once
|
183
|
+
def find_form_key(id, force = false)
|
184
|
+
result = nil
|
185
|
+
begin
|
186
|
+
page = agent(force).get(mechanize_url(id))
|
146
187
|
match = page.body.match(FORM_KEY_EXPRESSION)
|
188
|
+
if page.meta.first and (page.title == "Redirecting" || match.nil?)
|
189
|
+
page = page.meta.first.click
|
190
|
+
match = page.body.match(FORM_KEY_EXPRESSION)
|
191
|
+
end
|
192
|
+
result = match.captures.first if match
|
193
|
+
rescue Exception => e #
|
194
|
+
puts "ERROR: #{e.to_s}"
|
195
|
+
result = find_form_key(id, true) unless force # try again, unless we have already tried again
|
196
|
+
end
|
197
|
+
result
|
198
|
+
end
|
199
|
+
|
200
|
+
# returns {:fields => fields, :body => body, :properties => properties, :description => description}
|
201
|
+
def parse(html, options, &block)
|
202
|
+
title = html.xpath("//h1[@class='ss-form-title']").first.text
|
203
|
+
description = html.css(".ss-form-desc").first
|
204
|
+
description = description ? description.children.to_html : ""
|
205
|
+
|
206
|
+
properties = {}
|
207
|
+
fields = []
|
208
|
+
|
209
|
+
html.css("div.ss-item").each do |item|
|
210
|
+
tag = ""
|
211
|
+
required = false
|
212
|
+
classes = item["class"].split(/\s+/)
|
213
|
+
classes.each do |clazz|
|
214
|
+
if clazz =~ /ss-(text|paragraph-text|grid|scale|radio|checkbox)/
|
215
|
+
tag = from_google_tag($1)
|
216
|
+
elsif clazz =~ /ss-item-required/
|
217
|
+
required = true
|
218
|
+
end
|
219
|
+
end
|
220
|
+
entry = item.xpath("div[@class='ss-form-entry']").first
|
221
|
+
label = entry.xpath("label[@class='ss-q-title']").first
|
222
|
+
next unless label
|
223
|
+
title = label.text.gsub(/:[^:]+$/, "").gsub(/\n+.*/, "") # remove trailing :, then trailing newline stuff
|
224
|
+
help = entry.xpath("label[@class='ss-q-help']").first.text
|
225
|
+
single = (item["class"].to_s =~ /ss-(grid|scale)/).nil?
|
226
|
+
type = single ? "single" : "group"
|
227
|
+
column = label.text.downcase.gsub(/[^a-z0-9\-]/, "")
|
228
|
+
input_name = label["for"].gsub("_", ".").squeeze("\.") + "." + type
|
229
|
+
|
230
|
+
value = []
|
231
|
+
if tag =~ /(radio|checkbox|range|text)/
|
232
|
+
item.css("* input[type=#{$1}]").each do |input|
|
233
|
+
value << input["value"]
|
234
|
+
end
|
235
|
+
elsif tag =~ /(select)/
|
236
|
+
item.css("* #{$1} option").each do |option|
|
237
|
+
value << option["value"]
|
238
|
+
end
|
239
|
+
end
|
240
|
+
# still need to handle defaults
|
241
|
+
keys = {
|
242
|
+
:id => input_name.gsub(/[^\d]/, ""),
|
243
|
+
:key => column,
|
244
|
+
:tag => tag,
|
245
|
+
:title => title,
|
246
|
+
:help => help,
|
247
|
+
:required => required,
|
248
|
+
:value => value # need defaults somehow
|
249
|
+
}
|
250
|
+
if tag == "range"
|
251
|
+
keys[:left_label] = item.css("* td.ss-leftlabel").text
|
252
|
+
keys[:right_label] = item.css("* td.ss-rightlabel").text
|
253
|
+
end
|
254
|
+
fields << keys
|
255
|
+
properties[column] = input_name
|
147
256
|
end
|
148
|
-
|
149
|
-
|
150
|
-
puts "ERROR: #{e.to_s}"
|
151
|
-
nil
|
257
|
+
|
258
|
+
{:fields => fields, :body => html, :properties => properties, :description => description}
|
152
259
|
end
|
260
|
+
|
261
|
+
def add_redirect(doc, options, &block)
|
262
|
+
action = doc.xpath("//form").first
|
263
|
+
return if action.nil?
|
264
|
+
action = action["action"].to_s
|
265
|
+
submit_key = action.gsub(self.submit_url, "")
|
266
|
+
|
267
|
+
form = doc.xpath("//form").first
|
268
|
+
|
269
|
+
form["enctype"] = "multipart/form-data"
|
270
|
+
|
271
|
+
# don't have time to build this correctly
|
272
|
+
redirect = options[:redirect]
|
273
|
+
if redirect
|
274
|
+
form["action"] = redirect[:url]
|
275
|
+
if redirect.has_key?(:params)
|
276
|
+
redirect[:params].each do |k,v|
|
277
|
+
hidden_node = doc.create_element('input')
|
278
|
+
hidden_node["name"] = k.to_s
|
279
|
+
hidden_node["type"] = "hidden"
|
280
|
+
hidden_node["value"] = v.to_s
|
281
|
+
form.children.first.add_previous_sibling(hidden_node)
|
282
|
+
end
|
283
|
+
end
|
284
|
+
end
|
285
|
+
|
286
|
+
hidden_node = doc.create_element('input')
|
287
|
+
hidden_node["name"] = "submit_key"
|
288
|
+
hidden_node["type"] = "hidden"
|
289
|
+
hidden_node["value"] = submit_key
|
290
|
+
form.children.first.add_previous_sibling(hidden_node)
|
291
|
+
|
292
|
+
put_node = doc.create_element('input')
|
293
|
+
put_node["name"] = "_method"
|
294
|
+
put_node["type"] = "hidden"
|
295
|
+
put_node["value"] = "put"
|
296
|
+
form.children.first.add_previous_sibling(put_node)
|
297
|
+
|
298
|
+
form
|
299
|
+
end
|
300
|
+
|
153
301
|
end
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
else
|
163
|
-
response.body
|
302
|
+
|
303
|
+
# construct(:redirect => {:url => "/our-forms", :params => {:one => "hello"}})
|
304
|
+
def construct(options = {}, &block)
|
305
|
+
return if body
|
306
|
+
raise "I NEED A FORM KEY (#{self.title.to_s})!" unless self.form_key
|
307
|
+
parts = fetch(options, &block)
|
308
|
+
parts.each do |k, v|
|
309
|
+
self.send("#{k}=", v)
|
164
310
|
end
|
311
|
+
parts.delete(:body)
|
312
|
+
parts[:form_key] = form_key
|
313
|
+
self.attributes.merge!(parts)
|
314
|
+
body
|
165
315
|
end
|
166
316
|
|
167
|
-
|
168
|
-
def get(options = {}, &block)
|
169
|
-
raise "I NEED A FORM KEY!" unless self.form_key
|
317
|
+
def fetch(options = {}, &block)
|
170
318
|
response = client.get(show_url)
|
171
319
|
if response.is_a?(Net::HTTPSuccess) || response.is_a?(GData::HTTP::Response)
|
172
|
-
|
320
|
+
self.class.parse(Nokogiri::HTML(response.body), options, &block)
|
173
321
|
else
|
174
322
|
response
|
175
323
|
end
|
176
324
|
end
|
177
325
|
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
@properties = {}
|
188
|
-
html.css("div.ss-item").each do |item|
|
189
|
-
entry = item.xpath("div[@class='ss-form-entry']").first
|
190
|
-
label = entry.xpath("label[@class='ss-q-title']").first
|
191
|
-
next unless label
|
192
|
-
single = (item["class"].to_s =~ /ss-(grid|scale)/).nil?
|
193
|
-
type = single ? "single" : "group"
|
194
|
-
column = label.text.downcase.gsub(/[^a-z0-9\-]/, "")
|
195
|
-
input_name = label["for"].gsub("_", ".").squeeze("\.") + "." + type
|
196
|
-
@properties[column] = input_name
|
197
|
-
end
|
198
|
-
|
199
|
-
html
|
200
|
-
end
|
201
|
-
|
202
|
-
def add_redirect(doc, options, &block)
|
203
|
-
action = doc.xpath("//form").first
|
204
|
-
return if action.nil?
|
205
|
-
action = action["action"].to_s
|
206
|
-
submit_key = action.gsub(self.submit_url, "")
|
207
|
-
|
208
|
-
form = doc.xpath("//form").first
|
209
|
-
|
210
|
-
form["enctype"] = "multipart/form-data"
|
211
|
-
|
212
|
-
# don't have time to build this correctly
|
213
|
-
redirect = options[:redirect] || self.redirect_to
|
214
|
-
if redirect
|
215
|
-
form["action"] = redirect[:url]
|
216
|
-
if redirect.has_key?(:params)
|
217
|
-
redirect[:params].each do |k,v|
|
218
|
-
hidden_node = doc.create_element('input')
|
219
|
-
hidden_node["name"] = k.to_s
|
220
|
-
hidden_node["type"] = "hidden"
|
221
|
-
hidden_node["value"] = v.to_s
|
222
|
-
form.children.first.add_previous_sibling(hidden_node)
|
223
|
-
end
|
224
|
-
end
|
326
|
+
def submit(form_key, params, options)
|
327
|
+
uri = URI.parse(submit_url)
|
328
|
+
req = Net::HTTP::Post.new("#{uri.path}?#{uri.query}")
|
329
|
+
req.form_data = params
|
330
|
+
response = Net::HTTP.new(uri.host).start {|h| h.request(req)}
|
331
|
+
if response.is_a?(Net::HTTPSuccess) || response.is_a?(GData::HTTP::Response)
|
332
|
+
self.class.parse(Nokogiri::HTML(response.body), options).to_html
|
333
|
+
else
|
334
|
+
response.body
|
225
335
|
end
|
226
|
-
|
227
|
-
hidden_node = doc.create_element('input')
|
228
|
-
hidden_node["name"] = "submit_key"
|
229
|
-
hidden_node["type"] = "hidden"
|
230
|
-
hidden_node["value"] = submit_key
|
231
|
-
form.children.first.add_previous_sibling(hidden_node)
|
232
|
-
|
233
|
-
put_node = doc.create_element('input')
|
234
|
-
put_node["name"] = "_method"
|
235
|
-
put_node["type"] = "hidden"
|
236
|
-
put_node["value"] = "put"
|
237
|
-
form.children.first.add_previous_sibling(put_node)
|
238
|
-
|
239
|
-
form
|
240
336
|
end
|
241
337
|
end
|
data/rails/init.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "googletastic"
|
@@ -21,25 +21,32 @@ describe Googletastic::Form do
|
|
21
21
|
|
22
22
|
it "should respond_to?(:find_with_google_form) dynamic class methods" do
|
23
23
|
Form.respond_to?(:find_with_google_form).should == true
|
24
|
-
pending
|
25
24
|
end
|
26
25
|
end
|
27
26
|
|
28
27
|
describe "mechanize" do
|
29
28
|
it "it should get the formkey via mechanize" do
|
30
29
|
form = Googletastic::Form.first
|
31
|
-
formkey = form.
|
32
|
-
puts "
|
30
|
+
formkey = form.form_key
|
31
|
+
puts "Form Key: #{formkey.to_s}"
|
33
32
|
formkey.should_not be_nil
|
34
33
|
end
|
35
34
|
|
36
35
|
it "should remove unnecessary html from form" do
|
37
36
|
form = Googletastic::Form.first
|
38
|
-
form.
|
37
|
+
form.construct
|
39
38
|
body = form.body
|
40
|
-
puts "BODY!: #{body.to_s}"
|
41
39
|
body.should_not == nil
|
42
40
|
end
|
41
|
+
|
42
|
+
it "should get 'properties', 'fields', and 'description'" do
|
43
|
+
form = Googletastic::Form.first
|
44
|
+
form.construct
|
45
|
+
form.properties.should_not == nil
|
46
|
+
form.fields.should_not == nil
|
47
|
+
form.description.should_not == nil
|
48
|
+
puts form.inspect
|
49
|
+
end
|
43
50
|
end
|
44
51
|
|
45
52
|
|
metadata
CHANGED
@@ -5,8 +5,8 @@ version: !ruby/object:Gem::Version
|
|
5
5
|
segments:
|
6
6
|
- 0
|
7
7
|
- 0
|
8
|
-
-
|
9
|
-
version: 0.0.
|
8
|
+
- 5
|
9
|
+
version: 0.0.5
|
10
10
|
platform: ruby
|
11
11
|
authors:
|
12
12
|
- Lance Pollard
|
@@ -14,7 +14,7 @@ autorequire:
|
|
14
14
|
bindir: bin
|
15
15
|
cert_chain: []
|
16
16
|
|
17
|
-
date: 2010-03-22
|
17
|
+
date: 2010-03-22 20:12:47 -07:00
|
18
18
|
default_executable:
|
19
19
|
dependencies:
|
20
20
|
- !ruby/object:Gem::Dependency
|
@@ -158,6 +158,7 @@ files:
|
|
158
158
|
- spec/googletastic/youtube_spec.rb
|
159
159
|
- spec/spec.opts
|
160
160
|
- spec/spec_helper.rb
|
161
|
+
- rails/init.rb
|
161
162
|
has_rdoc: true
|
162
163
|
homepage: http://github.com/viatropos/googletastic
|
163
164
|
licenses: []
|