neves-ruby_picasa 0.2.3
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/History.txt +22 -0
- data/README.txt +79 -0
- data/lib/ruby_picasa/types.rb +315 -0
- data/lib/ruby_picasa.rb +397 -0
- data/spec/ruby_picasa/types_spec.rb +303 -0
- data/spec/ruby_picasa_spec.rb +382 -0
- data/spec/sample/album.atom +141 -0
- data/spec/sample/recent.atom +111 -0
- data/spec/sample/search.atom +99 -0
- data/spec/sample/user.atom +107 -0
- data/spec/spec.opts +2 -0
- data/spec/spec_helper.rb +13 -0
- metadata +76 -0
data/lib/ruby_picasa.rb
ADDED
@@ -0,0 +1,397 @@
|
|
1
|
+
require 'objectify_xml'
|
2
|
+
require 'objectify_xml/atom'
|
3
|
+
require 'cgi'
|
4
|
+
require 'net/http'
|
5
|
+
require 'net/https'
|
6
|
+
require File.join(File.dirname(__FILE__), 'ruby_picasa/types')
|
7
|
+
|
8
|
+
module RubyPicasa
|
9
|
+
VERSION = '0.2.3'
|
10
|
+
|
11
|
+
class PicasaError < StandardError
|
12
|
+
end
|
13
|
+
|
14
|
+
class PicasaTokenError < PicasaError
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# == Authorization
|
19
|
+
#
|
20
|
+
# RubyPicasa makes authorizing a Rails app easy. It is a two step process:
|
21
|
+
#
|
22
|
+
# First redirect the user to the authorization url, if the user authorizes your
|
23
|
+
# application, Picasa will redirect the user back to the url you specify (in
|
24
|
+
# this case authorize_picasa_url).
|
25
|
+
#
|
26
|
+
# Next, pass the Rails request object to the authorize_token method which will
|
27
|
+
# make the api call to upgrade the token and if successful return an initialized
|
28
|
+
# Picasa session object ready to use. The token object can be retrieved from the
|
29
|
+
# token attribute.
|
30
|
+
#
|
31
|
+
# class PicasaController < ApplicationController
|
32
|
+
# def request_authorization
|
33
|
+
# redirect_to Picasa.authorization_url(authorize_picasa_url)
|
34
|
+
# end
|
35
|
+
#
|
36
|
+
# def authorize
|
37
|
+
# if Picasa.token_in_request?(request)
|
38
|
+
# begin
|
39
|
+
# picasa = Picasa.authorize_request(request)
|
40
|
+
# current_user.picasa_token = picasa.token
|
41
|
+
# current_user.save
|
42
|
+
# flash[:notice] = 'Picasa authorization complete'
|
43
|
+
# redirect_to picasa_path
|
44
|
+
# rescue PicasaTokenError => e
|
45
|
+
# #
|
46
|
+
# @error = e.message
|
47
|
+
# render
|
48
|
+
# end
|
49
|
+
# end
|
50
|
+
# end
|
51
|
+
# end
|
52
|
+
#
|
53
|
+
class Picasa
|
54
|
+
class << self
|
55
|
+
# The user must be redirected to this address to authorize the application
|
56
|
+
# to access their Picasa account. The token_from_request and
|
57
|
+
# authorize_request methods can be used to handle the resulting redirect
|
58
|
+
# from Picasa.
|
59
|
+
def authorization_url(return_to_url, request_session = true, secure = false, authsub_url = nil)
|
60
|
+
session = request_session ? '1' : '0'
|
61
|
+
secure = secure ? '1' : '0'
|
62
|
+
return_to_url = CGI.escape(return_to_url)
|
63
|
+
url = authsub_url || 'http://www.google.com/accounts/AuthSubRequest'
|
64
|
+
"#{ url }?scope=http%3A%2F%2F#{ host }%2Fdata%2F&session=#{ session }&secure=#{ secure }&next=#{ return_to_url }"
|
65
|
+
end
|
66
|
+
|
67
|
+
# Takes a Rails request object and extracts the token from it. This would
|
68
|
+
# happen in the action that is pointed to by the return_to_url argument
|
69
|
+
# when the authorization_url is created.
|
70
|
+
def token_from_request(request)
|
71
|
+
if token = request.parameters['token']
|
72
|
+
return token
|
73
|
+
else
|
74
|
+
raise RubyPicasa::PicasaTokenError, 'No Picasa authorization token was found.'
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def token_in_request?(request)
|
79
|
+
request.parameters['token']
|
80
|
+
end
|
81
|
+
|
82
|
+
# Takes a Rails request object as in token_from_request, then makes the
|
83
|
+
# token authorization request to produce the permanent token. This will
|
84
|
+
# only work if request_session was true when you created the
|
85
|
+
# authorization_url.
|
86
|
+
def authorize_request(request)
|
87
|
+
p = Picasa.new(token_from_request(request))
|
88
|
+
p.authorize_token!
|
89
|
+
p
|
90
|
+
end
|
91
|
+
|
92
|
+
# The url to make requests to without the protocol or path.
|
93
|
+
def host
|
94
|
+
@host ||= 'picasaweb.google.com'
|
95
|
+
end
|
96
|
+
|
97
|
+
# In the unlikely event that you need to access this api on a different url,
|
98
|
+
# you can set it here. It defaults to picasaweb.google.com
|
99
|
+
def host=(h)
|
100
|
+
@host = h
|
101
|
+
end
|
102
|
+
|
103
|
+
# A simple test used to determine if a given resource id is it's full
|
104
|
+
# identifier url. This is not intended to be a general purpose method as the
|
105
|
+
# test is just a check for the http/https protocol prefix.
|
106
|
+
def is_url?(path)
|
107
|
+
path.to_s =~ %r{\Ahttps?://}
|
108
|
+
end
|
109
|
+
|
110
|
+
# For more on possible options and their meanings, see:
|
111
|
+
# http://code.google.com/apis/picasaweb/reference.html
|
112
|
+
#
|
113
|
+
# The following values are valid for the thumbsize and imgmax query
|
114
|
+
# parameters and are embeddable on a webpage. These images are available as
|
115
|
+
# both cropped(c) and uncropped(u) sizes by appending c or u to the size.
|
116
|
+
# As an example, to retrieve a 72 pixel image that is cropped, you would
|
117
|
+
# specify 72c, while to retrieve the uncropped image, you would specify 72u
|
118
|
+
# for the thumbsize or imgmax query parameter values.
|
119
|
+
#
|
120
|
+
# 32, 48, 64, 72, 144, 160
|
121
|
+
#
|
122
|
+
# The following values are valid for the thumbsize and imgmax query
|
123
|
+
# parameters and are embeddable on a webpage. These images are available as
|
124
|
+
# only uncropped(u) sizes by appending u to the size or just passing the
|
125
|
+
# size value without appending anything.
|
126
|
+
#
|
127
|
+
# 200, 288, 320, 400, 512, 576, 640, 720, 800
|
128
|
+
#
|
129
|
+
# The following values are valid for the thumbsize and imgmax query
|
130
|
+
# parameters and are not embeddable on a webpage. These image sizes are
|
131
|
+
# only available in uncropped format and are accessed using only the size
|
132
|
+
# (no u is appended to the size).
|
133
|
+
#
|
134
|
+
# 912, 1024, 1152, 1280, 1440, 1600
|
135
|
+
#
|
136
|
+
def path(args = {})
|
137
|
+
path, options = parse_url(args)
|
138
|
+
if path.nil?
|
139
|
+
path = ["/data/feed/api"]
|
140
|
+
if args[:user_id] == 'all'
|
141
|
+
path += ["all"]
|
142
|
+
else
|
143
|
+
path += ["user", CGI.escape(args[:user_id] || 'default')]
|
144
|
+
end
|
145
|
+
path += ['albumid', CGI.escape(args[:album_id])] if args[:album_id]
|
146
|
+
path += ['album', CGI.escape(args[:album])] if args[:album]
|
147
|
+
path = path.join('/')
|
148
|
+
end
|
149
|
+
options['kind'] = 'photo' if args[:recent_photos] or args[:album_id] or args[:album]
|
150
|
+
if args[:thumbsize] and not args[:thumbsize].split(/,/).all? { |s| RubyPicasa::Photo::VALID.include?(s) }
|
151
|
+
raise RubyPicasa::PicasaError, 'Invalid thumbsize.'
|
152
|
+
end
|
153
|
+
if args[:imgmax] and not RubyPicasa::Photo::VALID.include?(args[:imgmax])
|
154
|
+
raise RubyPicasa::PicasaError, 'Invalid imgmax.'
|
155
|
+
end
|
156
|
+
[:max_results, :start_index, :tag, :q, :kind,
|
157
|
+
:access, :thumbsize, :imgmax, :bbox, :l].each do |arg|
|
158
|
+
options[arg.to_s.dasherize] = args[arg] if args[arg]
|
159
|
+
end
|
160
|
+
if options.empty?
|
161
|
+
path
|
162
|
+
else
|
163
|
+
[path, options.map { |k, v| [k.to_s, CGI.escape(v.to_s)].join('=') }.join('&')].join('?')
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
# builder helper for creating a public RubyPicasa::User
|
168
|
+
def public_user(user_id, options = {})
|
169
|
+
Picasa.new.user(user_id, options)
|
170
|
+
end
|
171
|
+
|
172
|
+
# builder helper for creating a public RubyPicasa::Album using an album_id
|
173
|
+
def public_album_by_id(user_id, album_id, options = {})
|
174
|
+
options[:user_id] = user_id
|
175
|
+
Picasa.new.album(album_id, options)
|
176
|
+
end
|
177
|
+
|
178
|
+
# builder helper for creating a public RubyPicasa::Album using an album_name
|
179
|
+
def public_album_by_name(user_id, album_name, options = {})
|
180
|
+
options[:user_id] = user_id
|
181
|
+
Picasa.new.album_by_name(album_name, options)
|
182
|
+
end
|
183
|
+
|
184
|
+
private
|
185
|
+
|
186
|
+
# Extract the path and a hash of key/value pairs from a given url with
|
187
|
+
# optional query string.
|
188
|
+
def parse_url(args)
|
189
|
+
url = args[:url]
|
190
|
+
url ||= args[:user_id] if is_url?(args[:user_id])
|
191
|
+
url ||= args[:album_id] if is_url?(args[:album_id])
|
192
|
+
if url
|
193
|
+
uri = URI.parse(url)
|
194
|
+
path = uri.path
|
195
|
+
options = {}
|
196
|
+
if uri.query
|
197
|
+
uri.query.split('&').each do |query|
|
198
|
+
k, v = query.split('=')
|
199
|
+
options[k] = CGI.unescape(v)
|
200
|
+
end
|
201
|
+
end
|
202
|
+
[path, options]
|
203
|
+
else
|
204
|
+
[nil, {}]
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
# The AuthSub token currently in use.
|
210
|
+
attr_reader :token
|
211
|
+
|
212
|
+
def initialize(token = nil)
|
213
|
+
@token = token
|
214
|
+
@request_cache = {}
|
215
|
+
end
|
216
|
+
|
217
|
+
# Attempt to upgrade the current AuthSub token to a permanent one. This only
|
218
|
+
# works if the Picasa session is initialized with a single use token.
|
219
|
+
def authorize_token!
|
220
|
+
http = Net::HTTP.new("www.google.com", 443)
|
221
|
+
http.use_ssl = true
|
222
|
+
response = http.get('/accounts/accounts/AuthSubSessionToken', auth_header)
|
223
|
+
token = response.body.scan(/Token=(.*)/).flatten.compact.first
|
224
|
+
if token
|
225
|
+
@token = token
|
226
|
+
else
|
227
|
+
raise RubyPicasa::PicasaTokenError, 'The request to upgrade to a session token failed.'
|
228
|
+
end
|
229
|
+
@token
|
230
|
+
end
|
231
|
+
|
232
|
+
# Retrieve a RubyPicasa::User record including all user albums.
|
233
|
+
def user(user_id_or_url = nil, options = {})
|
234
|
+
options = make_options(:user_id, user_id_or_url, options)
|
235
|
+
get(options)
|
236
|
+
end
|
237
|
+
|
238
|
+
# Retrieve a RubyPicasa::Album record. If you pass an id or a feed url it will
|
239
|
+
# include all photos. If you pass an entry url, it will not include photos.
|
240
|
+
def album(album_id_or_url, options = {})
|
241
|
+
options = make_options(:album_id, album_id_or_url, options)
|
242
|
+
get(options)
|
243
|
+
end
|
244
|
+
|
245
|
+
# Retrieve a RubyPicasa::Album record by using the album name
|
246
|
+
def album_by_name(album_name, options = {})
|
247
|
+
options = make_options(:album, album_name, options)
|
248
|
+
get(options)
|
249
|
+
end
|
250
|
+
|
251
|
+
# This request does not require authentication. Returns a RubyPicasa::Search
|
252
|
+
# object containing the first 10 matches. You can call #next and #previous to
|
253
|
+
# navigate the paginated results on the Search object.
|
254
|
+
def search(q, options = {})
|
255
|
+
h = {}
|
256
|
+
h[:max_results] = 10
|
257
|
+
h[:user_id] = 'all'
|
258
|
+
h[:kind] = 'photo'
|
259
|
+
# merge options over h, but merge q over options
|
260
|
+
get(h.merge(options).merge(:q => q))
|
261
|
+
end
|
262
|
+
|
263
|
+
# Retrieve a RubyPicasa object determined by the type of xml results returned
|
264
|
+
# by Picasa. Any supported type of RubyPicasa resource can be requested with
|
265
|
+
# this method.
|
266
|
+
def get_url(url, options = {})
|
267
|
+
options = make_options(:url, url, options)
|
268
|
+
get(options)
|
269
|
+
end
|
270
|
+
|
271
|
+
# Retrieve a RubyPicasa::RecentPhotos object, essentially a User object which
|
272
|
+
# contains photos instead of albums.
|
273
|
+
def recent_photos(user_id_or_url, options = {})
|
274
|
+
options = make_options(:user_id, user_id_or_url, options)
|
275
|
+
options[:recent_photos] = true
|
276
|
+
get(options)
|
277
|
+
end
|
278
|
+
|
279
|
+
# Retrieves the user's albums and finds the first one with a matching title.
|
280
|
+
# Returns a RubyPicasa::Album object.
|
281
|
+
def album_by_title(title, options = {})
|
282
|
+
if a = user.albums.find { |a| title === a.title }
|
283
|
+
a.load options
|
284
|
+
end
|
285
|
+
end
|
286
|
+
|
287
|
+
# Returns the raw xml from Picasa. See the Picasa.path method for valid
|
288
|
+
# options.
|
289
|
+
def xml(options = {})
|
290
|
+
http = Net::HTTP.new(Picasa.host, 80)
|
291
|
+
path = Picasa.path(options)
|
292
|
+
response = http.get(path, auth_header)
|
293
|
+
if response.code =~ /20[01]/
|
294
|
+
response.body
|
295
|
+
end
|
296
|
+
end
|
297
|
+
|
298
|
+
private
|
299
|
+
|
300
|
+
# If the value parameter is a hash, treat it as the options hash, otherwise
|
301
|
+
# insert the value into the hash with the key specified.
|
302
|
+
#
|
303
|
+
# Uses merge to ensure that a new hash object is returned to prevent caller's
|
304
|
+
# has from accidentally being modified.
|
305
|
+
def make_options(key, value, options)
|
306
|
+
if value.is_a? Hash
|
307
|
+
{}.merge value
|
308
|
+
else
|
309
|
+
options ||= {}
|
310
|
+
options.merge(key => value)
|
311
|
+
end
|
312
|
+
end
|
313
|
+
|
314
|
+
# Combines the cached xml request with the class_from_xml factory. See the
|
315
|
+
# Picasa.path method for valid options.
|
316
|
+
def get(options = {})
|
317
|
+
with_cache(options) do |xml|
|
318
|
+
class_from_xml(xml)
|
319
|
+
end
|
320
|
+
end
|
321
|
+
|
322
|
+
# Returns the header data needed to make AuthSub requests.
|
323
|
+
def auth_header
|
324
|
+
if token
|
325
|
+
{ "Authorization" => %{AuthSub token="#{ token }"} }
|
326
|
+
else
|
327
|
+
{}
|
328
|
+
end
|
329
|
+
end
|
330
|
+
|
331
|
+
# Caches the raw xml returned from the API. Keyed on request url.
|
332
|
+
def with_cache(options)
|
333
|
+
path = Picasa.path(options)
|
334
|
+
@request_cache.delete(path) if options[:reload]
|
335
|
+
xml = nil
|
336
|
+
if @request_cache.has_key? path
|
337
|
+
xml = @request_cache[path]
|
338
|
+
else
|
339
|
+
xml = @request_cache[path] = xml(options)
|
340
|
+
end
|
341
|
+
if xml
|
342
|
+
yield xml
|
343
|
+
end
|
344
|
+
end
|
345
|
+
|
346
|
+
# Returns the first xml element in the document (see
|
347
|
+
# Objectify::Xml.first_element) with the xml data types of the feed and first entry
|
348
|
+
# element in the document, used to determine which RubyPicasa object should
|
349
|
+
# be initialized to handle the data.
|
350
|
+
def xml_data(xml)
|
351
|
+
if xml = Objectify::Xml.first_element(xml)
|
352
|
+
# There is something wrong with Nokogiri xpath/css search with
|
353
|
+
# namespaces. If you are searching a document that has namespaces,
|
354
|
+
# it's impossible to match any elements in the root xmlns namespace.
|
355
|
+
# Matching just on attributes works though.
|
356
|
+
feed, entry = xml.search('//*[@term][@scheme]', xml.namespaces)
|
357
|
+
feed_scheme = feed['term'] if feed
|
358
|
+
entry_scheme = entry['term'] if entry
|
359
|
+
[xml, feed_scheme, entry_scheme]
|
360
|
+
end
|
361
|
+
end
|
362
|
+
|
363
|
+
# Initialize the correct RubyPicasa object depending on the type of feed and
|
364
|
+
# entries in the document.
|
365
|
+
def class_from_xml(xml)
|
366
|
+
xml, feed_scheme, entry_scheme = xml_data(xml)
|
367
|
+
if xml
|
368
|
+
r = case feed_scheme
|
369
|
+
when /#user$/
|
370
|
+
case entry_scheme
|
371
|
+
when /#album$/
|
372
|
+
RubyPicasa::User.new(xml, self)
|
373
|
+
when /#photo$/
|
374
|
+
RubyPicasa::RecentPhotos.new(xml, self)
|
375
|
+
end
|
376
|
+
when /#album$/
|
377
|
+
case entry_scheme
|
378
|
+
when nil, /#photo$/
|
379
|
+
RubyPicasa::Album.new(xml, self)
|
380
|
+
end
|
381
|
+
when /#photo$/
|
382
|
+
case entry_scheme
|
383
|
+
when /#photo$/
|
384
|
+
RubyPicasa::Search.new(xml, self)
|
385
|
+
when nil
|
386
|
+
RubyPicasa::Photo.new(xml, self)
|
387
|
+
end
|
388
|
+
end
|
389
|
+
if r
|
390
|
+
r.session = self
|
391
|
+
r
|
392
|
+
else
|
393
|
+
raise RubyPicasa::PicasaError, "Unknown feed type\n feed: #{ feed_scheme }\n entry: #{ entry_scheme }"
|
394
|
+
end
|
395
|
+
end
|
396
|
+
end
|
397
|
+
end
|
@@ -0,0 +1,303 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), '../spec_helper')
|
2
|
+
|
3
|
+
include RubyPicasa
|
4
|
+
|
5
|
+
describe 'a RubyPicasa document', :shared => true do
|
6
|
+
it 'should have a feed_id' do
|
7
|
+
@object.feed_id.should_not be_nil
|
8
|
+
end
|
9
|
+
|
10
|
+
it 'should have an author' do
|
11
|
+
unless @no_author
|
12
|
+
@object.author.should_not be_nil
|
13
|
+
@object.author.name.should == 'Liz'
|
14
|
+
@object.author.uri.should == 'http://picasaweb.google.com/liz'
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'should get links by name' do
|
19
|
+
@object.link('abc').should be_nil
|
20
|
+
@object.link('self').href.should_not be_nil
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'should do nothing for previous and next' do
|
24
|
+
@object.previous.should be_nil if @object.link('previous').nil?
|
25
|
+
@object.next.should be_nil if @object.link('next').nil?
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'should get the feed' do
|
29
|
+
@object.session.expects(:get_url).with(@object.feed_id.
|
30
|
+
gsub(/entry/, 'feed').
|
31
|
+
gsub(/default/, 'liz'), {})
|
32
|
+
@object.feed
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'should have links' do
|
36
|
+
@object.links.should_not be_empty
|
37
|
+
@object.links.each do |l|
|
38
|
+
l.should be_an_instance_of(Objectify::Atom::Link)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
describe 'session' do
|
43
|
+
it 'should return @session' do
|
44
|
+
@object.session = :sess
|
45
|
+
@object.session.should == :sess
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'should get the parent session' do
|
49
|
+
@object.session = nil
|
50
|
+
@parent.expects(:session).returns(:parent_sess)
|
51
|
+
@object.session.should == :parent_sess
|
52
|
+
end
|
53
|
+
|
54
|
+
it 'should be nil if no parent' do
|
55
|
+
@object.session = nil
|
56
|
+
@object.expects(:parent).returns nil
|
57
|
+
@object.session.should be_nil
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
|
63
|
+
describe User do
|
64
|
+
it_should_behave_like 'a RubyPicasa document'
|
65
|
+
|
66
|
+
before :all do
|
67
|
+
@xml = open_file('user.atom').read
|
68
|
+
end
|
69
|
+
|
70
|
+
before do
|
71
|
+
@parent = mock('parent')
|
72
|
+
@object = @user = User.new(@xml, @parent)
|
73
|
+
@user.session = mock('session')
|
74
|
+
end
|
75
|
+
|
76
|
+
it 'should have albums' do
|
77
|
+
@user.albums.length.should == 1
|
78
|
+
@user.albums.first.should be_an_instance_of(Album)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
describe RecentPhotos do
|
83
|
+
it_should_behave_like 'a RubyPicasa document'
|
84
|
+
|
85
|
+
before :all do
|
86
|
+
@xml = open_file('recent.atom').read
|
87
|
+
end
|
88
|
+
|
89
|
+
before do
|
90
|
+
@parent = mock('parent')
|
91
|
+
@object = @album = RecentPhotos.new(@xml, @parent)
|
92
|
+
@album.session = mock('session')
|
93
|
+
end
|
94
|
+
|
95
|
+
it 'should have 1 photo' do
|
96
|
+
@album.photos.length.should == 1
|
97
|
+
@album.photos.first.should be_an_instance_of(Photo)
|
98
|
+
end
|
99
|
+
|
100
|
+
it 'should request next' do
|
101
|
+
@album.session.expects(:get_url).with('http://picasaweb.google.com/data/feed/api/user/liz?start-index=2&max-results=1&kind=photo').returns(:result)
|
102
|
+
@album.next.should == :result
|
103
|
+
end
|
104
|
+
|
105
|
+
it 'should not request previous on first page' do
|
106
|
+
@album.session.expects(:get_url).never
|
107
|
+
@album.previous.should be_nil
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
describe Album do
|
112
|
+
it_should_behave_like 'a RubyPicasa document'
|
113
|
+
|
114
|
+
before :all do
|
115
|
+
@xml = open_file('album.atom').read
|
116
|
+
end
|
117
|
+
|
118
|
+
before do
|
119
|
+
@parent = mock('parent')
|
120
|
+
@object = @album = Album.new(@xml, @parent)
|
121
|
+
@album.session = mock('session')
|
122
|
+
end
|
123
|
+
|
124
|
+
it 'should have a numeric id' do
|
125
|
+
@object.id.should_not be_nil
|
126
|
+
@object.id.to_s.should match(/\A\d+\Z/)
|
127
|
+
end
|
128
|
+
|
129
|
+
it 'should have 1 entry' do
|
130
|
+
@album.entries.length.should == 1
|
131
|
+
end
|
132
|
+
|
133
|
+
it 'should get links by name' do
|
134
|
+
@album.link('abc').should be_nil
|
135
|
+
@album.link('alternate').href.should == 'http://picasaweb.google.com/liz/Lolcats'
|
136
|
+
end
|
137
|
+
|
138
|
+
describe 'photos' do
|
139
|
+
it 'should use entries if available' do
|
140
|
+
@album.expects(:session).never
|
141
|
+
@album.photos.should == @album.entries
|
142
|
+
end
|
143
|
+
|
144
|
+
it 'should request photos if needed' do
|
145
|
+
@album.entries = []
|
146
|
+
new_album = mock('album', :entries => [:photo])
|
147
|
+
@album.session.expects(:get_url).with(@album.link(/feed/).href, {}).returns(new_album)
|
148
|
+
@album.photos.should == [:photo]
|
149
|
+
end
|
150
|
+
|
151
|
+
it 'should not request photos twice if there are none' do
|
152
|
+
@album.entries = []
|
153
|
+
new_album = mock('album', :entries => [])
|
154
|
+
@album.session.expects(:get_url).with(@album.link(/feed/).href, {}).times(1).returns(new_album)
|
155
|
+
@album.photos.should == []
|
156
|
+
# note that mocks are set to accept only one get_url request
|
157
|
+
@album.photos.should == []
|
158
|
+
end
|
159
|
+
|
160
|
+
it 'should not request photos if there is no session' do
|
161
|
+
@album.entries = []
|
162
|
+
@album.expects(:session).returns(nil)
|
163
|
+
@album.photos.should == []
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
it 'should be public' do
|
168
|
+
@album.public?.should be_true
|
169
|
+
end
|
170
|
+
|
171
|
+
it 'should not be private' do
|
172
|
+
@album.private?.should be_false
|
173
|
+
end
|
174
|
+
|
175
|
+
describe 'first Photo' do
|
176
|
+
before do
|
177
|
+
@photo = @album.entries.first
|
178
|
+
@photo.should be_an_instance_of(Photo)
|
179
|
+
end
|
180
|
+
|
181
|
+
it 'should have a parent' do
|
182
|
+
@photo.parent.should == @album
|
183
|
+
end
|
184
|
+
|
185
|
+
it 'should not have an author' do
|
186
|
+
@photo.author.should be_nil
|
187
|
+
end
|
188
|
+
|
189
|
+
it 'should have a content' do
|
190
|
+
@photo.content.should be_an_instance_of(PhotoUrl)
|
191
|
+
end
|
192
|
+
|
193
|
+
it 'should have 3 thumbnails' do
|
194
|
+
@photo.thumbnails.length.should == 3
|
195
|
+
@photo.thumbnails.each do |t|
|
196
|
+
t.should be_an_instance_of(ThumbnailUrl)
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
it 'should have a numeric id' do
|
201
|
+
@object.id.should_not be_nil
|
202
|
+
@object.id.to_s.should match(/\A\d+\Z/)
|
203
|
+
end
|
204
|
+
|
205
|
+
it 'should have a default url' do
|
206
|
+
@photo.url.should == 'http://lh5.ggpht.com/liz/SKXR5BoXabI/AAAAAAAAAzs/tJQefyM4mFw/invisible_bike.jpg'
|
207
|
+
end
|
208
|
+
|
209
|
+
it 'should have thumbnail urls' do
|
210
|
+
@photo.url('72').should == 'http://lh5.ggpht.com/liz/SKXR5BoXabI/AAAAAAAAAzs/tJQefyM4mFw/s72/invisible_bike.jpg'
|
211
|
+
end
|
212
|
+
|
213
|
+
it 'should have a default url with options true' do
|
214
|
+
@photo.url(nil, true).should == [
|
215
|
+
'http://lh5.ggpht.com/liz/SKXR5BoXabI/AAAAAAAAAzs/tJQefyM4mFw/invisible_bike.jpg',
|
216
|
+
{ :width => 410, :height => 295 }
|
217
|
+
]
|
218
|
+
end
|
219
|
+
|
220
|
+
it 'should have a default url with options' do
|
221
|
+
@photo.url(nil, :id => 'p').should == [
|
222
|
+
'http://lh5.ggpht.com/liz/SKXR5BoXabI/AAAAAAAAAzs/tJQefyM4mFw/invisible_bike.jpg',
|
223
|
+
{ :width => 410, :height => 295, :id => 'p' }
|
224
|
+
]
|
225
|
+
end
|
226
|
+
|
227
|
+
it 'should have a default url with options first' do
|
228
|
+
@photo.url(:id => 'p').should == [
|
229
|
+
'http://lh5.ggpht.com/liz/SKXR5BoXabI/AAAAAAAAAzs/tJQefyM4mFw/invisible_bike.jpg',
|
230
|
+
{ :width => 410, :height => 295, :id => 'p' }
|
231
|
+
]
|
232
|
+
end
|
233
|
+
|
234
|
+
it 'should have thumbnail urls with options' do
|
235
|
+
@photo.url('72', {:class => 'x'}).should == [
|
236
|
+
'http://lh5.ggpht.com/liz/SKXR5BoXabI/AAAAAAAAAzs/tJQefyM4mFw/s72/invisible_bike.jpg',
|
237
|
+
{ :width => 72, :height => 52, :class => 'x' }
|
238
|
+
]
|
239
|
+
end
|
240
|
+
|
241
|
+
it 'should have thumbnail info' do
|
242
|
+
@photo.thumbnail('72').width.should == 72
|
243
|
+
end
|
244
|
+
|
245
|
+
it 'should retrieve valid thumbnail info' do
|
246
|
+
photo = mock('photo')
|
247
|
+
thumb = mock('thumb')
|
248
|
+
photo.expects(:thumbnails).returns([thumb])
|
249
|
+
@photo.session.expects(:get_url).with('http://picasaweb.google.com/data/feed/api/user/liz/albumid/5228155363249705041/photoid/5234820919508560306',
|
250
|
+
{:thumbsize => '32c'}).returns(photo)
|
251
|
+
@photo.thumbnail('32c').should == thumb
|
252
|
+
end
|
253
|
+
|
254
|
+
it 'should retrieve valid thumbnail info and handle not found' do
|
255
|
+
@photo.session.expects(:get_url).with('http://picasaweb.google.com/data/feed/api/user/liz/albumid/5228155363249705041/photoid/5234820919508560306',
|
256
|
+
{:thumbsize => '32c'}).returns(nil)
|
257
|
+
@photo.thumbnail('32c').should be_nil
|
258
|
+
end
|
259
|
+
|
260
|
+
it 'should convert the thumbnail url into another size without crop' do
|
261
|
+
@photo.tb(160).should == "http://lh5.ggpht.com/liz/SKXR5BoXabI/AAAAAAAAAzs/tJQefyM4mFw/s160/invisible_bike.jpg"
|
262
|
+
end
|
263
|
+
|
264
|
+
it 'should convert the thumbnail url into another size with crop' do
|
265
|
+
@photo.tb(160, true).should == "http://lh5.ggpht.com/liz/SKXR5BoXabI/AAAAAAAAAzs/tJQefyM4mFw/s160-c/invisible_bike.jpg"
|
266
|
+
end
|
267
|
+
end
|
268
|
+
end
|
269
|
+
|
270
|
+
describe Search do
|
271
|
+
it_should_behave_like 'a RubyPicasa document'
|
272
|
+
|
273
|
+
before :all do
|
274
|
+
@xml = open_file('search.atom').read
|
275
|
+
end
|
276
|
+
|
277
|
+
before do
|
278
|
+
@no_author = true
|
279
|
+
@parent = mock('parent')
|
280
|
+
@object = @search = Search.new(@xml, @parent)
|
281
|
+
@search.session = mock('session')
|
282
|
+
end
|
283
|
+
|
284
|
+
it 'should have 1 entry' do
|
285
|
+
@search.entries.length.should == 1
|
286
|
+
@search.entries.first.should be_an_instance_of(Photo)
|
287
|
+
end
|
288
|
+
|
289
|
+
it 'should alias entries to photos' do
|
290
|
+
@search.photos.should == @search.entries
|
291
|
+
end
|
292
|
+
|
293
|
+
it 'should request next' do
|
294
|
+
@search.session.expects(:get_url).with('http://picasaweb.google.com/data/feed/api/all?q=puppy&start-index=3&max-results=1').returns(:result)
|
295
|
+
@search.next.should == :result
|
296
|
+
end
|
297
|
+
|
298
|
+
it 'should request previous' do
|
299
|
+
@search.session.expects(:get_url).with('http://picasaweb.google.com/data/feed/api/all?q=puppy&start-index=1&max-results=1').returns(:result)
|
300
|
+
@search.previous.should == :result
|
301
|
+
end
|
302
|
+
end
|
303
|
+
|