googletastic 0.0.1
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/README.textile +47 -0
- data/Rakefile +103 -0
- data/lib/googletastic/access_rule.rb +45 -0
- data/lib/googletastic/album.rb +22 -0
- data/lib/googletastic/app_engine.rb +103 -0
- data/lib/googletastic/attendee.rb +7 -0
- data/lib/googletastic/base.rb +46 -0
- data/lib/googletastic/calendar.rb +60 -0
- data/lib/googletastic/comment.rb +3 -0
- data/lib/googletastic/document.rb +219 -0
- data/lib/googletastic/event.rb +149 -0
- data/lib/googletastic/ext/file.rb +219 -0
- data/lib/googletastic/ext/pretty_print.xsl +47 -0
- data/lib/googletastic/ext/xml.rb +10 -0
- data/lib/googletastic/ext.rb +1 -0
- data/lib/googletastic/form.rb +131 -0
- data/lib/googletastic/group.rb +44 -0
- data/lib/googletastic/helpers/document.rb +70 -0
- data/lib/googletastic/helpers/event.rb +26 -0
- data/lib/googletastic/helpers/form.rb +102 -0
- data/lib/googletastic/helpers.rb +18 -0
- data/lib/googletastic/image.rb +121 -0
- data/lib/googletastic/mixins/actions.rb +113 -0
- data/lib/googletastic/mixins/attributes.rb +27 -0
- data/lib/googletastic/mixins/finders.rb +78 -0
- data/lib/googletastic/mixins/namespaces.rb +42 -0
- data/lib/googletastic/mixins/parsing.rb +68 -0
- data/lib/googletastic/mixins/requesting.rb +84 -0
- data/lib/googletastic/mixins.rb +5 -0
- data/lib/googletastic/person.rb +41 -0
- data/lib/googletastic/spreadsheet.rb +35 -0
- data/lib/googletastic/sync/document.rb +120 -0
- data/lib/googletastic/sync/form.rb +106 -0
- data/lib/googletastic/sync.rb +46 -0
- data/lib/googletastic/thumbnail.rb +7 -0
- data/lib/googletastic/youtube.rb +25 -0
- data/lib/googletastic.rb +115 -0
- data/spec/benchmarks/document_benchmark.rb +40 -0
- data/spec/config.yml +6 -0
- data/spec/fixtures/data/Doing business in the eMarketPlace.doc +0 -0
- data/spec/fixtures/data/basic.txt +1 -0
- data/spec/fixtures/data/calendar.list.xml +64 -0
- data/spec/fixtures/data/doc_as_html.html +3097 -0
- data/spec/fixtures/data/doc_as_html_html.html +0 -0
- data/spec/fixtures/data/doclist.xml +76 -0
- data/spec/fixtures/data/document.single.xml +30 -0
- data/spec/fixtures/data/end.xml +446 -0
- data/spec/fixtures/data/event.list.xml +62 -0
- data/spec/fixtures/data/form.html +66 -0
- data/spec/fixtures/data/group.list.xml +58 -0
- data/spec/fixtures/data/person.list.xml +53 -0
- data/spec/fixtures/data/photo.list.xml +111 -0
- data/spec/fixtures/data/sample_upload.mp4 +0 -0
- data/spec/fixtures/data/spreadsheet.list.xml +43 -0
- data/spec/fixtures/models/document.rb +7 -0
- data/spec/fixtures/models/event.rb +3 -0
- data/spec/fixtures/models/form.rb +6 -0
- data/spec/fixtures/models/test_model.rb +3 -0
- data/spec/fixtures/results/test.txt +185 -0
- data/spec/googletastic/access_rule_spec.rb +13 -0
- data/spec/googletastic/album_spec.rb +9 -0
- data/spec/googletastic/app_engine_spec.rb +12 -0
- data/spec/googletastic/base_spec.rb +49 -0
- data/spec/googletastic/calendar_spec.rb +11 -0
- data/spec/googletastic/document_spec.rb +163 -0
- data/spec/googletastic/event_spec.rb +34 -0
- data/spec/googletastic/form_spec.rb +43 -0
- data/spec/googletastic/group_spec.rb +9 -0
- data/spec/googletastic/image_spec.rb +9 -0
- data/spec/googletastic/person_spec.rb +9 -0
- data/spec/googletastic/post_spec.rb +46 -0
- data/spec/googletastic/spreadsheet_spec.rb +9 -0
- data/spec/googletastic/youtube_spec.rb +58 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +25 -0
- metadata +187 -0
data/README.textile
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
h1. Googletastic
|
2
|
+
|
3
|
+
Making it more than easy to save everything on Google, so all your app has to do is say how to define pretty urls and formatting.
|
4
|
+
|
5
|
+
h2. Installing
|
6
|
+
|
7
|
+
<code>
|
8
|
+
sudo gem install googletastic
|
9
|
+
</code>
|
10
|
+
|
11
|
+
h3. Installing from Source
|
12
|
+
|
13
|
+
<pre><code>
|
14
|
+
git clone git@github.com:viatropos/googletastic.git
|
15
|
+
cd googletastic
|
16
|
+
rake install
|
17
|
+
</code></pre>
|
18
|
+
|
19
|
+
h2. Authentication
|
20
|
+
|
21
|
+
Google's API's have "two types of accounts"http://code.google.com/apis/accounts/docs/AuthForInstalledApps.html#Request:
|
22
|
+
|
23
|
+
# Regular (for individuals): "GOOGLE"
|
24
|
+
# Hosted (for Google Apps): "HOSTED" (or "HOSTED_OR_GOOGLE")
|
25
|
+
|
26
|
+
Google's APIs have "3 types of authentication":http://code.google.com/apis/accounts/index.html:
|
27
|
+
|
28
|
+
# "ClientLogin":http://code.google.com/apis/accounts/docs/AuthForInstalledApps.html: For single users
|
29
|
+
# "OpenAuth":http://code.google.com/apis/accounts/docs/OAuth.html: For "registering an app":http://code.google.com/apis/accounts/docs/RegistrationForWebAppsAuto.html to automatically login. You need to "register your domain here":https://www.google.com/accounts/ManageDomains
|
30
|
+
# "AuthSub":http://code.google.com/apis/accounts/docs/AuthSub.html
|
31
|
+
|
32
|
+
You can permanently unlock the CAPTCHA for a computer by going "here":https://www.google.com/a/accd.org/UnlockCaptcha.
|
33
|
+
|
34
|
+
"Working with Authsub":http://code.google.com/apis/accounts/docs/AuthSub.html#WorkingAuthSub
|
35
|
+
|
36
|
+
If this is for Google Apps, you must go to Google Docs Settings in the Admin Panel, and make sure this is selected: "Users can share documents outside this domain, but will receive a warning each time". The most restrictive, "Users cannot share documents outside this domain", won't allow you to view documents at all. Also, don't select "Require mydomain.com sign-in to view this form." for google forms.
|
37
|
+
|
38
|
+
h2. Top 10 Things I Need the Google Data API to Have
|
39
|
+
|
40
|
+
"Submit Your Ideas to Google!!!":http://productideas.appspot.com/#15/e=220cb&t=2192a&f=22150
|
41
|
+
|
42
|
+
# Partial Responses for everything.
|
43
|
+
So we can get only the data we need. This way we can poll our google docs for any changes and run processes on them.
|
44
|
+
# Publish the docs as plain text.
|
45
|
+
# Textile and Markdown support
|
46
|
+
# Better font support in PDF documents
|
47
|
+
# Everything Should Be TAGGABLE
|
data/Rakefile
ADDED
@@ -0,0 +1,103 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require "rake/rdoctask"
|
3
|
+
require 'rake/gempackagetask'
|
4
|
+
require File.join(File.expand_path(File.dirname(__FILE__)),'lib','googletastic')
|
5
|
+
|
6
|
+
begin
|
7
|
+
require 'spec/rake/spectask'
|
8
|
+
rescue LoadError
|
9
|
+
puts <<-EOS
|
10
|
+
To use rspec for testing you must install rspec gem:
|
11
|
+
gem install rspec
|
12
|
+
EOS
|
13
|
+
exit(0)
|
14
|
+
end
|
15
|
+
|
16
|
+
def files(path, from = __FILE__, &block)
|
17
|
+
Dir.glob(File.join(File.dirname(from), path)) {|file| yield file}
|
18
|
+
end
|
19
|
+
|
20
|
+
APP_ROOT = defined?(RAILS_ROOT) ? RAILS_ROOT : File.dirname(__FILE__)
|
21
|
+
RAILS_ROOT = File.dirname(__FILE__)
|
22
|
+
|
23
|
+
# http://docs.rubygems.org/read/chapter/20
|
24
|
+
spec = Gem::Specification.new do |s|
|
25
|
+
s.name = "googletastic"
|
26
|
+
s.author = "Lance Pollard"
|
27
|
+
s.version = Googletastic::VERSION
|
28
|
+
s.date = "Mon Mar 22 20:12:47 -0700 2010"
|
29
|
+
s.summary = "More than Syncing Rails Apps with the Google Data API"
|
30
|
+
s.homepage = "http://github.com/viatropos/googletastic"
|
31
|
+
s.email = "lancejpollard@gmail.com"
|
32
|
+
s.description = "Googletastic: A New Way of Googling"
|
33
|
+
s.has_rdoc = true
|
34
|
+
s.rubyforge_project = "googletastic"
|
35
|
+
s.platform = Gem::Platform::RUBY
|
36
|
+
s.files = %w(README.textile Rakefile) +
|
37
|
+
Dir["{googletastic,lib,spec}/**/*"] -
|
38
|
+
Dir["spec/tmp"]
|
39
|
+
s.extra_rdoc_files = %w(README.textile)
|
40
|
+
s.require_path = "lib"
|
41
|
+
s.add_dependency("nokogiri")
|
42
|
+
s.add_dependency("activesupport", ">= 2.3.5")
|
43
|
+
s.add_dependency("activerecord", ">= 2.3.5")
|
44
|
+
s.add_dependency("gdata")
|
45
|
+
end
|
46
|
+
|
47
|
+
desc "Create .gemspec file (useful for github)"
|
48
|
+
task :gemspec do
|
49
|
+
File.open("#{spec.name}.gemspec", "w") do |f|
|
50
|
+
f.puts spec.to_ruby
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
desc "Build the gem into the current directory"
|
55
|
+
task :gem => :gemspec do
|
56
|
+
`gem build #{spec.name}.gemspec`
|
57
|
+
end
|
58
|
+
|
59
|
+
desc "Print a list of the files to be put into the gem"
|
60
|
+
task :manifest => :clean do
|
61
|
+
File.open("Manifest", "w") do |f|
|
62
|
+
spec.files.each do |file|
|
63
|
+
f.puts file
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
Rake::GemPackageTask.new(spec) do |pkg|
|
69
|
+
pkg.gem_spec = spec
|
70
|
+
end
|
71
|
+
|
72
|
+
desc "Install the gem locally"
|
73
|
+
task :install => [:package] do
|
74
|
+
sh %{sudo gem install pkg/googletastic-#{Googletastic::VERSION} --no-ri --no-rdoc}
|
75
|
+
end
|
76
|
+
|
77
|
+
desc "Run all specs"
|
78
|
+
Spec::Rake::SpecTask.new('spec') do |t|
|
79
|
+
t.spec_opts = ["--color"]
|
80
|
+
t.spec_files = FileList['spec/**/*_spec.rb']
|
81
|
+
end
|
82
|
+
|
83
|
+
desc "Print specdocs"
|
84
|
+
Spec::Rake::SpecTask.new(:doc) do |t|
|
85
|
+
t.spec_opts = ["--format", "specdoc"]
|
86
|
+
t.spec_files = FileList['spec/*_spec.rb']
|
87
|
+
end
|
88
|
+
|
89
|
+
desc "Generate the rdoc"
|
90
|
+
Rake::RDocTask.new do |rdoc|
|
91
|
+
files = ["README.textile", "lib/**/*.rb"]
|
92
|
+
rdoc.rdoc_files.add(files)
|
93
|
+
rdoc.main = "README.textile"
|
94
|
+
rdoc.title = "Googletastic: A New Way of Googling"
|
95
|
+
end
|
96
|
+
|
97
|
+
desc "Run the rspec"
|
98
|
+
task :default => :spec
|
99
|
+
|
100
|
+
desc "Run Googletastic Benchmarks"
|
101
|
+
task :benchmark do
|
102
|
+
files("spec/benchmarks/*") {|file| system("ruby #{file}") }
|
103
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
class Googletastic::AccessRule < Googletastic::Base
|
2
|
+
|
3
|
+
attr_accessor :title, :agent, :role, :email
|
4
|
+
|
5
|
+
class << self
|
6
|
+
|
7
|
+
def build_url(options)
|
8
|
+
base = nil
|
9
|
+
if options.has_key?(:document_id)
|
10
|
+
options[:client] = Googletastic::Document.client
|
11
|
+
base = "http://docs.google.com/feeds/acl/private/full/#{options[:document_id]}"
|
12
|
+
end
|
13
|
+
options[:url] = base
|
14
|
+
super(options)
|
15
|
+
end
|
16
|
+
|
17
|
+
def unmarshall(xml)
|
18
|
+
records = xml.xpath("//atom:entry", ns_tag("atom")).collect do |record|
|
19
|
+
title = id = value = nil # reset
|
20
|
+
id = record.xpath("atom:id", ns_tag("atom")).text
|
21
|
+
title = record.xpath("atom:title", ns_tag("atom")).text
|
22
|
+
scope = record.xpath("gAcl:scope", ns_tag("gAcl")).first
|
23
|
+
role = record.xpath("gAcl:role", ns_tag("gAcl")).first["value"].to_s
|
24
|
+
Googletastic::AccessRule.new(
|
25
|
+
:id => id,
|
26
|
+
:title => title,
|
27
|
+
:email => scope["value"].to_s,
|
28
|
+
:agent => scope["type"].to_s,
|
29
|
+
:role => role
|
30
|
+
)
|
31
|
+
end
|
32
|
+
records
|
33
|
+
end
|
34
|
+
|
35
|
+
def marshall(record)
|
36
|
+
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
|
41
|
+
def has_access?(email)
|
42
|
+
self.email == email
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
class Googletastic::Album < Googletastic::Base
|
2
|
+
|
3
|
+
class << self
|
4
|
+
|
5
|
+
def client_class
|
6
|
+
"Photos"
|
7
|
+
end
|
8
|
+
|
9
|
+
def index_url
|
10
|
+
"http://picasaweb.google.com/data/feed/api/user/default"
|
11
|
+
end
|
12
|
+
|
13
|
+
def unmarshall(xml)
|
14
|
+
records = xml.xpath("//atom:entry", ns_tag("atom")).collect do |record|
|
15
|
+
|
16
|
+
end
|
17
|
+
records
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
module GData
|
2
|
+
module Client
|
3
|
+
|
4
|
+
# Client class to wrap working with the TBD App Engine API.
|
5
|
+
class AppEngine < Base
|
6
|
+
|
7
|
+
def initialize(options = {})
|
8
|
+
options[:clientlogin_service] = 'ah'
|
9
|
+
super(options)
|
10
|
+
end
|
11
|
+
|
12
|
+
# override so we can get cookie
|
13
|
+
def get(url)
|
14
|
+
set_cookie(url)
|
15
|
+
response = self.make_request(:get, url)
|
16
|
+
if response.status_code == 302
|
17
|
+
# if it's redirecting us, we need to get the cookie
|
18
|
+
new_cookie(url)
|
19
|
+
# now we can make the request again
|
20
|
+
response = self.make_request(:get, url)
|
21
|
+
end
|
22
|
+
response
|
23
|
+
end
|
24
|
+
|
25
|
+
def new_cookie(url)
|
26
|
+
cookie = get_cookie(url)
|
27
|
+
headers["Cookie"] = cookie
|
28
|
+
Googletastic.credentials[:cookie] = cookie
|
29
|
+
# save the cookie to a file since we're
|
30
|
+
# not in the browser!
|
31
|
+
storage = Googletastic.keys.has_key?(:storage) ? Googletastic.keys[:storage] : :filesystem
|
32
|
+
|
33
|
+
# heroku can't store anything on filesystem
|
34
|
+
if storage == :filesystem
|
35
|
+
File.open("config/gdata.yml", 'w') do |file|
|
36
|
+
file.write Googletastic.credentials.to_yaml
|
37
|
+
end
|
38
|
+
elsif storage == :environment
|
39
|
+
ENV["GOOGLETASTIC_GAE_COOKIE"] = cookie
|
40
|
+
end
|
41
|
+
cookie
|
42
|
+
end
|
43
|
+
|
44
|
+
def get_cookie(url)
|
45
|
+
uri = URI.parse(url)
|
46
|
+
url_for_cookie = "#{uri.scheme}://#{uri.host}/_ah/login?continue=#{url}&auth=#{auth_handler.token}"
|
47
|
+
response = self.make_request(:get, url_for_cookie)
|
48
|
+
cookie = response.headers["set-cookie"].split('; ',2)[0]
|
49
|
+
cookie
|
50
|
+
end
|
51
|
+
|
52
|
+
def set_cookie(url)
|
53
|
+
if Googletastic.credentials[:cookie]
|
54
|
+
headers["Cookie"] = Googletastic.credentials[:cookie]
|
55
|
+
return headers["Cookie"]
|
56
|
+
else
|
57
|
+
new_cookie(url)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# globally push to google app engine, using the python library
|
62
|
+
# pushes files to gae
|
63
|
+
# required options:
|
64
|
+
# folder => where the python command executes
|
65
|
+
# username, password
|
66
|
+
def push(username, password, options = {})
|
67
|
+
options.symbolize_keys!
|
68
|
+
raise "Please define local GAE Folder" unless options[:folder] and File.exists?(options[:folder])
|
69
|
+
sleep 0.5
|
70
|
+
# update GAE
|
71
|
+
puts "Pushing to GAE..."
|
72
|
+
cmd = "appcfg.py update #{options[:folder]}"
|
73
|
+
original_values = [STDOUT.sync, STDERR.sync, $expect_verbose]
|
74
|
+
STDOUT.sync = true
|
75
|
+
STDERR.sync = true
|
76
|
+
$expect_verbose = true
|
77
|
+
PTY.spawn("#{cmd} 2>&1") do | input, output, pid |
|
78
|
+
begin
|
79
|
+
input.expect("Email:") do
|
80
|
+
output.puts("#{username}\n")
|
81
|
+
end
|
82
|
+
input.expect("Password") do
|
83
|
+
output.write("#{password}\n")
|
84
|
+
end
|
85
|
+
while ((str = input.gets) != nil)
|
86
|
+
puts str
|
87
|
+
end
|
88
|
+
rescue Exception => e
|
89
|
+
puts "GAE Error... #{e.inspect}"
|
90
|
+
end
|
91
|
+
end
|
92
|
+
STDOUT.sync = original_values[0]
|
93
|
+
STDERR.sync = original_values[1]
|
94
|
+
$expect_verbose = original_values[2]
|
95
|
+
sleep 0.5
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
module Googletastic::AppEngine
|
102
|
+
|
103
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
class Googletastic::Base < Hash
|
2
|
+
|
3
|
+
include Googletastic::Mixins::Namespaces
|
4
|
+
include Googletastic::Mixins::Attributes
|
5
|
+
include Googletastic::Mixins::Requesting
|
6
|
+
include Googletastic::Mixins::Parsing
|
7
|
+
include Googletastic::Mixins::Finders
|
8
|
+
include Googletastic::Mixins::Actions
|
9
|
+
|
10
|
+
# ID's are specifically the hash key for each entry.
|
11
|
+
# They don't include the url for finding the document,
|
12
|
+
# which can be inferred from the "action" you're taking on it,
|
13
|
+
# and the general api.
|
14
|
+
attr_accessor :id
|
15
|
+
attr_accessor :acl
|
16
|
+
attr_accessor :created_at, :updated_at
|
17
|
+
attr_accessor :attachment_path # for docs, images...
|
18
|
+
# classes/records it is synced with
|
19
|
+
attr_accessor :synced_with
|
20
|
+
attr_accessor :response
|
21
|
+
|
22
|
+
def synced_with
|
23
|
+
@synced_with ||= []
|
24
|
+
@synced_with
|
25
|
+
end
|
26
|
+
|
27
|
+
def initialize(attributes = {})
|
28
|
+
self.attributes = attributes unless attributes.nil?
|
29
|
+
end
|
30
|
+
|
31
|
+
class << self # Class methods
|
32
|
+
|
33
|
+
# This is referring to the gdata gem's implementation,
|
34
|
+
# which more closely maps to the google api
|
35
|
+
# override if you name classes differently.
|
36
|
+
def client_class
|
37
|
+
self.to_s.split("::").last
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
|
42
|
+
def to_xml
|
43
|
+
self.class.marshall(self)
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
class Googletastic::Calendar < Googletastic::Base
|
2
|
+
|
3
|
+
attr_accessor :created_at, :updated_at, :title, :role
|
4
|
+
attr_accessor :color, :hidden, :selected, :timezone
|
5
|
+
|
6
|
+
class << self
|
7
|
+
|
8
|
+
def index_url
|
9
|
+
"http://www.google.com/calendar/feeds/default/allcalendars/full"
|
10
|
+
end
|
11
|
+
|
12
|
+
def show_url(id)
|
13
|
+
"http://www.google.com/calendar/embed?src=#{id}"
|
14
|
+
end
|
15
|
+
|
16
|
+
def unmarshall(xml)
|
17
|
+
records = xml.xpath("//atom:entry", ns_tag("atom")).collect do |record|
|
18
|
+
id = record.xpath("atom:id", ns_tag("atom")).first.text
|
19
|
+
title = record.xpath("atom:title", ns_tag("atom")).first.text
|
20
|
+
created_at = record.xpath("atom:published", ns_tag("atom")).first.text
|
21
|
+
updated_at = record.xpath("atom:updated", ns_tag("atom")).first.text
|
22
|
+
|
23
|
+
role = record.xpath("gCal:accesslevel", ns_tag("gCal")).first["value"]
|
24
|
+
color = record.xpath("gCal:color", ns_tag("gCal")).first["value"]
|
25
|
+
hidden = record.xpath("gCal:hidden", ns_tag("gCal")).first["value"]
|
26
|
+
selected = record.xpath("gCal:selected", ns_tag("gCal")).first["value"]
|
27
|
+
timezone = record.xpath("gCal:timezone", ns_tag("gCal")).first["value"]
|
28
|
+
|
29
|
+
Googletastic::Calendar.new(
|
30
|
+
:id => id,
|
31
|
+
:title => title,
|
32
|
+
:created_at => created_at,
|
33
|
+
:updated_at => updated_at,
|
34
|
+
:role => role,
|
35
|
+
:color => color,
|
36
|
+
:selected => selected,
|
37
|
+
:timezone => timezone
|
38
|
+
)
|
39
|
+
end
|
40
|
+
records
|
41
|
+
end
|
42
|
+
|
43
|
+
def marshall(record)
|
44
|
+
Nokogiri::XML::Builder.new { |xml|
|
45
|
+
xml.entry(ns_xml("atom", "gCal")) {
|
46
|
+
if record.id
|
47
|
+
xml.id {
|
48
|
+
xml.text "#{ID}#{record.id}"
|
49
|
+
}
|
50
|
+
end
|
51
|
+
xml.title {
|
52
|
+
xml.text record.title
|
53
|
+
}
|
54
|
+
}
|
55
|
+
}.to_xml
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
@@ -0,0 +1,219 @@
|
|
1
|
+
# "Anonymous modules have no name to be referenced by"
|
2
|
+
# If you have a class constant that's undefined
|
3
|
+
class Googletastic::Document < Googletastic::Base
|
4
|
+
|
5
|
+
ID = "https://docs.google.com/feeds/default/private/full/"
|
6
|
+
FEED_BASE = 'http://docs.google.com/feeds' unless defined?(FEED_BASE)
|
7
|
+
FEED = 'http://docs.google.com/feeds/documents/private/full' unless defined?(FEED)
|
8
|
+
BASE_URL = "http://docs.google.com" unless defined?(BASE_URL)
|
9
|
+
DOCS_DOWNLOAD = 'download/documents/Export'
|
10
|
+
CSV = "text/csv" unless defined?(CSV)
|
11
|
+
TSV = "text/tab-separated-values" unless defined?(TSV)
|
12
|
+
TAB = "text/tab-separated-values" unless defined?(TAB)
|
13
|
+
HTML = "text/html" unless defined?(HTML)
|
14
|
+
HTM = "text/html" unless defined?(HTM)
|
15
|
+
DOC = "application/msword" unless defined?(DOC)
|
16
|
+
DOCX = "application/vnd.openxmlformats-officedocument.wordprocessingml.document" unless defined?(DOCX)
|
17
|
+
ODS = "application/x-vnd.oasis.opendocument.spreadsheet" unless defined?(ODS)
|
18
|
+
ODT = "application/vnd.oasis.opendocument.text" unless defined?(ODT)
|
19
|
+
RTF = "application/rtf" unless defined?(RTF)
|
20
|
+
SXW = "application/vnd.sun.xml.writer" unless defined?(SXW)
|
21
|
+
TXT = "text/plain" unless defined?(TXT)
|
22
|
+
XLS = "application/vnd.ms-excel" unless defined?(XLS)
|
23
|
+
XLSX = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" unless defined?(XLSX)
|
24
|
+
PDF = "application/pdf" unless defined?(PDF)
|
25
|
+
PPT = "application/vnd.ms-powerpoint" unless defined?(PPT)
|
26
|
+
PPS = "application/vnd.ms-powerpoint" unless defined?(PPS)
|
27
|
+
|
28
|
+
attr_accessor :title, :kind, :categories, :resource_id, :ext
|
29
|
+
|
30
|
+
def has_access?(email)
|
31
|
+
self.class.first(:url => update_url)
|
32
|
+
end
|
33
|
+
|
34
|
+
attr_reader :body
|
35
|
+
def body
|
36
|
+
return @body if @body
|
37
|
+
@body = Nokogiri::XML(view.body)
|
38
|
+
end
|
39
|
+
|
40
|
+
attr_reader :content
|
41
|
+
# returns the content within the body tags for the document
|
42
|
+
def content
|
43
|
+
return @content if @content
|
44
|
+
body.xpath("//img").each do |img_node|
|
45
|
+
if img_node["src"] =~ /File\?/
|
46
|
+
img_node["src"] = "#{BASE_URL}/#{img_node['src']}"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
body.xpath("//div[@id='google-view-footer']").each { |n| n.unlink }
|
50
|
+
@content = body.xpath("//div[@id='doc-contents']").first
|
51
|
+
end
|
52
|
+
|
53
|
+
def acl
|
54
|
+
return @acl if @acl
|
55
|
+
@acl = Googletastic::AccessRule.find(:all, :document_id => id)
|
56
|
+
end
|
57
|
+
|
58
|
+
# returns the styles for the document, in case you need them
|
59
|
+
def styles
|
60
|
+
|
61
|
+
end
|
62
|
+
|
63
|
+
def view
|
64
|
+
client.get(self.show_url)
|
65
|
+
end
|
66
|
+
|
67
|
+
def download(format = "pdf")
|
68
|
+
client.get(self.download_url + "&exportFormat=#{format}")
|
69
|
+
end
|
70
|
+
|
71
|
+
def new_record?
|
72
|
+
return false if !self.id.nil?
|
73
|
+
return self.class.first(:title => self.title, :exact_title => true).nil?
|
74
|
+
end
|
75
|
+
|
76
|
+
# Time.now.xmlschema
|
77
|
+
class << self
|
78
|
+
|
79
|
+
def client_class
|
80
|
+
"DocList"
|
81
|
+
end
|
82
|
+
|
83
|
+
def valid_queries
|
84
|
+
{
|
85
|
+
:title => "title",
|
86
|
+
:exact_title => "title-exact",
|
87
|
+
:opened_after => "opened-min",
|
88
|
+
:opened_before => "opened-max",
|
89
|
+
:edited_after => "edited-min",
|
90
|
+
:edited_before => "edited-max",
|
91
|
+
:owner => "owner",
|
92
|
+
:writer => "writer",
|
93
|
+
:reader => "reader",
|
94
|
+
:show_folders => "showfolders",
|
95
|
+
:show_deleted => "showdeleted",
|
96
|
+
:optical_character_recognition => "ocr",
|
97
|
+
:translate_to => "targetLanguage",
|
98
|
+
:language => "sourceLanguage",
|
99
|
+
:permanent_delete => "delete",
|
100
|
+
:convert => "convert",
|
101
|
+
:format => "exportFormat"
|
102
|
+
}.merge(super)
|
103
|
+
end
|
104
|
+
|
105
|
+
def valid_category?(value)
|
106
|
+
%w(document spreadsheet folder presentation pdf form).include?(value)
|
107
|
+
end
|
108
|
+
|
109
|
+
def index_url(ids = nil)
|
110
|
+
"http://docs.google.com/feeds/documents/private/full"
|
111
|
+
end
|
112
|
+
|
113
|
+
# docid (all lowercase)
|
114
|
+
def show_url(id)
|
115
|
+
"http://docs.google.com/View?docID=#{id}&revision=_latest"
|
116
|
+
end
|
117
|
+
|
118
|
+
def get_url(id)
|
119
|
+
"https://docs.google.com/feeds/default/private/full/#{id}"
|
120
|
+
end
|
121
|
+
|
122
|
+
def edit_url(id)
|
123
|
+
"http://docs.google.com/Doc?docid=#{id}"
|
124
|
+
end
|
125
|
+
|
126
|
+
def update_url(id)
|
127
|
+
if has_attachment?
|
128
|
+
"http://docs.google.com/feeds/media/private/full/#{self.id}"
|
129
|
+
else
|
130
|
+
"http://docs.google.com/feeds/documents/private/full/#{self.id}"
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
# docId (camelcase)
|
135
|
+
def download_url(id)
|
136
|
+
"http://docs.google.com/feeds/download/documents/Export?docId=#{id}"
|
137
|
+
end
|
138
|
+
|
139
|
+
def upload_url(id = nil)
|
140
|
+
index_url(id)
|
141
|
+
end
|
142
|
+
|
143
|
+
%w(document spreadsheet folder presentation pdf form).each do |category|
|
144
|
+
define_method category do
|
145
|
+
all(:url => "#{index_url}/-/#{category}")
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
def download(*args)
|
150
|
+
old_options = args.extract_options!
|
151
|
+
options = {}
|
152
|
+
options[:format] = old_options[:format] || "pdf"
|
153
|
+
options[:url] = download_url(args.first.is_a?(String) ? args.first : old_options[:id])
|
154
|
+
args << options
|
155
|
+
find(*args)
|
156
|
+
end
|
157
|
+
|
158
|
+
def upload(*args)
|
159
|
+
client.post_file(upload_url, args.first, TXT)
|
160
|
+
end
|
161
|
+
|
162
|
+
def build_url(options)
|
163
|
+
base = options.has_key?(:url) ? options[:url] : self.index_url
|
164
|
+
base.gsub!(/full$/, "expandAcl") if options.has_key?(:include) and options[:include].index(:acl)
|
165
|
+
base << "/#{options[:id]}" if options.has_key?(:id)
|
166
|
+
options[:url] = base
|
167
|
+
super(options)
|
168
|
+
end
|
169
|
+
|
170
|
+
def unmarshall(xml)
|
171
|
+
records = xml.xpath("//atom:entry", ns_tag("atom")).collect do |record|
|
172
|
+
kind = title = id = categories = value = nil # reset
|
173
|
+
title = record.xpath("atom:title", ns_tag("atom")).text
|
174
|
+
categories = record.xpath("atom:category", ns_tag("atom")).collect do |category|
|
175
|
+
value = category["label"].to_s
|
176
|
+
kind = value if !kind and valid_category?(value)
|
177
|
+
value
|
178
|
+
end
|
179
|
+
resource_id = record.xpath("gd:resourceId", ns_tag("gd")).text
|
180
|
+
id = resource_id.gsub(/#{kind}:/, "")
|
181
|
+
created_at = record.xpath("atom:published", ns_tag("atom")).text
|
182
|
+
updated_at = record.xpath("atom:updated", ns_tag("atom")).text
|
183
|
+
puts "UPDATED AT: #{updated_at.inspect}"
|
184
|
+
Googletastic::Document.new(
|
185
|
+
:id => id,
|
186
|
+
:title => title,
|
187
|
+
:categories => categories,
|
188
|
+
:kind => kind,
|
189
|
+
:resource_id => resource_id,
|
190
|
+
:ext => File.extname(title),
|
191
|
+
:created_at => DateTime.parse(created_at),
|
192
|
+
:updated_at => DateTime.parse(updated_at)
|
193
|
+
)
|
194
|
+
end
|
195
|
+
records
|
196
|
+
end
|
197
|
+
|
198
|
+
def marshall(record)
|
199
|
+
Nokogiri::XML::Builder.new { |xml|
|
200
|
+
xml.entry(ns_xml("atom", "exif", "openSearch")) {
|
201
|
+
if record.id
|
202
|
+
xml.id_ {
|
203
|
+
xml.text get_url(record.resource_id)
|
204
|
+
}
|
205
|
+
end
|
206
|
+
record.categories.each do |category|
|
207
|
+
xml.category(
|
208
|
+
:scheme => "http://schemas.google.com/g/2005#kind",
|
209
|
+
:term => "#{category}"
|
210
|
+
)
|
211
|
+
end
|
212
|
+
xml.title {
|
213
|
+
xml.text record.title
|
214
|
+
}
|
215
|
+
}
|
216
|
+
}.to_xml
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|