googletastic 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (76) hide show
  1. data/README.textile +47 -0
  2. data/Rakefile +103 -0
  3. data/lib/googletastic/access_rule.rb +45 -0
  4. data/lib/googletastic/album.rb +22 -0
  5. data/lib/googletastic/app_engine.rb +103 -0
  6. data/lib/googletastic/attendee.rb +7 -0
  7. data/lib/googletastic/base.rb +46 -0
  8. data/lib/googletastic/calendar.rb +60 -0
  9. data/lib/googletastic/comment.rb +3 -0
  10. data/lib/googletastic/document.rb +219 -0
  11. data/lib/googletastic/event.rb +149 -0
  12. data/lib/googletastic/ext/file.rb +219 -0
  13. data/lib/googletastic/ext/pretty_print.xsl +47 -0
  14. data/lib/googletastic/ext/xml.rb +10 -0
  15. data/lib/googletastic/ext.rb +1 -0
  16. data/lib/googletastic/form.rb +131 -0
  17. data/lib/googletastic/group.rb +44 -0
  18. data/lib/googletastic/helpers/document.rb +70 -0
  19. data/lib/googletastic/helpers/event.rb +26 -0
  20. data/lib/googletastic/helpers/form.rb +102 -0
  21. data/lib/googletastic/helpers.rb +18 -0
  22. data/lib/googletastic/image.rb +121 -0
  23. data/lib/googletastic/mixins/actions.rb +113 -0
  24. data/lib/googletastic/mixins/attributes.rb +27 -0
  25. data/lib/googletastic/mixins/finders.rb +78 -0
  26. data/lib/googletastic/mixins/namespaces.rb +42 -0
  27. data/lib/googletastic/mixins/parsing.rb +68 -0
  28. data/lib/googletastic/mixins/requesting.rb +84 -0
  29. data/lib/googletastic/mixins.rb +5 -0
  30. data/lib/googletastic/person.rb +41 -0
  31. data/lib/googletastic/spreadsheet.rb +35 -0
  32. data/lib/googletastic/sync/document.rb +120 -0
  33. data/lib/googletastic/sync/form.rb +106 -0
  34. data/lib/googletastic/sync.rb +46 -0
  35. data/lib/googletastic/thumbnail.rb +7 -0
  36. data/lib/googletastic/youtube.rb +25 -0
  37. data/lib/googletastic.rb +115 -0
  38. data/spec/benchmarks/document_benchmark.rb +40 -0
  39. data/spec/config.yml +6 -0
  40. data/spec/fixtures/data/Doing business in the eMarketPlace.doc +0 -0
  41. data/spec/fixtures/data/basic.txt +1 -0
  42. data/spec/fixtures/data/calendar.list.xml +64 -0
  43. data/spec/fixtures/data/doc_as_html.html +3097 -0
  44. data/spec/fixtures/data/doc_as_html_html.html +0 -0
  45. data/spec/fixtures/data/doclist.xml +76 -0
  46. data/spec/fixtures/data/document.single.xml +30 -0
  47. data/spec/fixtures/data/end.xml +446 -0
  48. data/spec/fixtures/data/event.list.xml +62 -0
  49. data/spec/fixtures/data/form.html +66 -0
  50. data/spec/fixtures/data/group.list.xml +58 -0
  51. data/spec/fixtures/data/person.list.xml +53 -0
  52. data/spec/fixtures/data/photo.list.xml +111 -0
  53. data/spec/fixtures/data/sample_upload.mp4 +0 -0
  54. data/spec/fixtures/data/spreadsheet.list.xml +43 -0
  55. data/spec/fixtures/models/document.rb +7 -0
  56. data/spec/fixtures/models/event.rb +3 -0
  57. data/spec/fixtures/models/form.rb +6 -0
  58. data/spec/fixtures/models/test_model.rb +3 -0
  59. data/spec/fixtures/results/test.txt +185 -0
  60. data/spec/googletastic/access_rule_spec.rb +13 -0
  61. data/spec/googletastic/album_spec.rb +9 -0
  62. data/spec/googletastic/app_engine_spec.rb +12 -0
  63. data/spec/googletastic/base_spec.rb +49 -0
  64. data/spec/googletastic/calendar_spec.rb +11 -0
  65. data/spec/googletastic/document_spec.rb +163 -0
  66. data/spec/googletastic/event_spec.rb +34 -0
  67. data/spec/googletastic/form_spec.rb +43 -0
  68. data/spec/googletastic/group_spec.rb +9 -0
  69. data/spec/googletastic/image_spec.rb +9 -0
  70. data/spec/googletastic/person_spec.rb +9 -0
  71. data/spec/googletastic/post_spec.rb +46 -0
  72. data/spec/googletastic/spreadsheet_spec.rb +9 -0
  73. data/spec/googletastic/youtube_spec.rb +58 -0
  74. data/spec/spec.opts +1 -0
  75. data/spec/spec_helper.rb +25 -0
  76. 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,7 @@
1
+ class Googletastic::Attendee < Hash # simple value object
2
+
3
+ include Googletastic::Mixins::Attributes
4
+
5
+ attr_accessor :email, :role, :name
6
+
7
+ 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,3 @@
1
+ class Googletastic::Comment < Googletastic::Base
2
+
3
+ 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