googletastic 0.0.4 → 0.0.5
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.
- 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: []
|