picasa 0.4.2 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
data/.travis.yml CHANGED
@@ -9,3 +9,7 @@ env:
9
9
  - XML_PARSER=libxml
10
10
  # does not work yet
11
11
  # - XML_PARSER=ox
12
+
13
+ matrix:
14
+ allow_failures:
15
+ - env: XML_PARSER=rexml
data/README.md CHANGED
@@ -19,17 +19,40 @@ client.album.list
19
19
 
20
20
  client.album.show("album_id")
21
21
  # => Picasa::Presenter::Album
22
+
23
+ client.photo.create("album_id", file_path: "path/to/my-photo.png")
24
+ # => Picasa::Presenter::Photo
22
25
  ```
23
26
 
24
27
  If password is specified, all requests will be authenticated.
25
- This affect results to contain private data.
28
+ This affect results to contain private data, however it can be controlled by `access` parameter.
29
+
30
+ ## Caveats
31
+
32
+ Currently picasa wont work with `ox` xml parser.
33
+ Using `rexml` parser wont return `etag` attribute properly.
34
+ I recommend to use `libxml` or `nokogiri`.
35
+
36
+ ## Extra
37
+
38
+ You can install thor script for uploading all photos from given directory:
39
+
40
+ ```
41
+ thor install https://github.com/morgoth/picasa/raw/master/extra/Thorfile --as picasa_uploader --force
42
+ ```
43
+
44
+ And then use it (it will create album taking title from folder name and upload all photos from that directory):
45
+
46
+ ```
47
+ GOOGLE_USER_ID=your.email@gmail.com GOOGLE_PASSWORD=secret thor picasa_uploader:upload_all path-to-folder-with-photos
48
+ ```
26
49
 
27
50
  ## Continuous Integration
28
51
  [![Build Status](https://secure.travis-ci.org/morgoth/picasa.png)](http://travis-ci.org/morgoth/picasa)
29
52
 
30
53
  ## Contributors
31
54
 
32
- * [Bram Wijnands](https://github.com/BRamBoo)
55
+ * [Bram Wijnands](https://github.com/Bram--)
33
56
  * [Rafael Souza](https://github.com/rafaels)
34
57
  * [jsaak](https://github.com/jsaak)
35
58
  * [Javier Guerra](https://github.com/javierg)
data/extra/Thorfile ADDED
@@ -0,0 +1,41 @@
1
+ require "picasa"
2
+
3
+ # Temporary requirement
4
+ MultiXml.parser = :libxml
5
+
6
+ class PicasaUploader < Thor
7
+ include Thor::Actions
8
+
9
+ desc "upload_all DIR", "Uploads all photos from given directory"
10
+ def upload_all(dir = File.basename(Dir.getwd))
11
+ require_credentials
12
+ album_presenter = create_album(File.basename(dir))
13
+ photos_number = 0
14
+ inside(dir, :verbose => true) do
15
+ Dir.entries(".").select { |e| e =~ /\.(jpg|jpeg|png|gif|bmp)$/i }.sort.each do |file|
16
+ create_photo(album_presenter, file)
17
+ photos_number += 1
18
+ end
19
+ end
20
+ say "Finished uploading #{photos_number} photos"
21
+ end
22
+
23
+ no_tasks do
24
+ def require_credentials
25
+ say "You must specify GOOGLE_USER_ID env variable" and exit unless ENV["GOOGLE_USER_ID"]
26
+ say "You must specify GOOGLE_PASSWORD env variable" and exit unless ENV["GOOGLE_PASSWORD"]
27
+ end
28
+
29
+ def create_album(album)
30
+ say("Creating album: #{album}")
31
+ client = Picasa::Client.new(user_id: ENV["GOOGLE_USER_ID"], password: ENV["GOOGLE_PASSWORD"])
32
+ client.album.create(title: album)
33
+ end
34
+
35
+ def create_photo(album, path)
36
+ say("Uploading photo #{path} to album #{album.title}")
37
+ client = Picasa::Client.new(user_id: ENV["GOOGLE_USER_ID"], password: ENV["GOOGLE_PASSWORD"])
38
+ client.photo.create(album.id, file_path: path)
39
+ end
40
+ end
41
+ end
data/lib/picasa.rb CHANGED
@@ -5,7 +5,11 @@ require "picasa/utils"
5
5
  require "picasa/exceptions"
6
6
  require "picasa/connection"
7
7
  require "picasa/client"
8
+ require "picasa/file"
9
+ require "picasa/template"
10
+
8
11
  require "picasa/api/album"
12
+ require "picasa/api/photo"
9
13
  require "picasa/api/tag"
10
14
 
11
15
  require "picasa/presenter/album"
@@ -1,22 +1,15 @@
1
+ require "picasa/api/base"
2
+
1
3
  module Picasa
2
4
  module API
3
- class Album
4
- attr_reader :user_id, :credentials
5
-
6
- # @param [Hash] credentials
7
- # @option credentials [String] :user_id google username/email
8
- # @option credentials [String] :password password for given username/email
9
- def initialize(credentials)
10
- if MultiXml.parser.to_s == "MultiXml::Parsers::Ox"
11
- raise StandardError, "MultiXml parser is set to :ox - picasa gem will not work with it currently, use one of: :libxml, :nokogiri, :rexml"
12
- end
13
- @user_id = credentials.fetch(:user_id)
14
- @credentials = credentials
15
- end
16
-
5
+ class Album < Base
17
6
  # Returns album list
18
7
  #
19
8
  # @param [Hash] options additional options included in request
9
+ # @option options [all, private, public, visible] :access which data should be retrieved when authenticated
10
+ # @option options [String] :fields which fields should be retrieved https://developers.google.com/gdata/docs/2.0/reference#PartialResponseRequest
11
+ # @option options [String, Integer] :max_results how many albums response should include
12
+ # @option options [String, Integer] :start_index 1-based index of the first result to be retrieved
20
13
  #
21
14
  # @return [Presenter::AlbumList]
22
15
  def list(options = {})
@@ -41,6 +34,44 @@ module Picasa
41
34
 
42
35
  Presenter::Album.new(parsed_body["feed"])
43
36
  end
37
+
38
+ # Creates album
39
+ #
40
+ # @param [Hash] params album parameters
41
+ # @option options [String] :title title of album (required)
42
+ # @option options [String] :summary summary of album
43
+ # @option options [String] :location location of album photos (i.e. Poland)
44
+ # @option options [String] :access [public, private, protected]
45
+ # @option options [String] :timestamp timestamp of album (default to now)
46
+ # @option options [String] :keywords keywords (i.e. "vacation, poland")
47
+ #
48
+ # @return [Presenter::Album]
49
+ def create(params = {})
50
+ params[:title] || raise(ArgumentError, "You must specify title")
51
+ params[:timestamp] ||= Time.now.to_i
52
+
53
+ template = Template.new(:new_album, params)
54
+ uri = URI.parse("/data/feed/api/user/#{user_id}")
55
+ parsed_body = Connection.new(credentials).post(uri.path, template.render)
56
+ Presenter::Album.new(parsed_body["entry"])
57
+ end
58
+
59
+ # Destroys given album
60
+ #
61
+ # @param [String] album_id album id
62
+ # @param [Hash] options request parameters
63
+ # @option options [String] :etag destroys only when ETag matches - protects before destroying other client changes
64
+ #
65
+ # @return [true]
66
+ # @raise [NotFoundError] raised when album cannot be found
67
+ # @raise [PreconditionFailedError] raised when ETag does not match
68
+ def destroy(album_id, options = {})
69
+ headers = {"If-Match" => options.fetch(:etag, "*")}
70
+ uri = URI.parse("/data/entry/api/user/#{user_id}/albumid/#{album_id}")
71
+ Connection.new(credentials).delete(uri.path, headers)
72
+ true
73
+ end
74
+ alias :delete :destroy
44
75
  end
45
76
  end
46
77
  end
@@ -0,0 +1,18 @@
1
+ module Picasa
2
+ module API
3
+ class Base
4
+ attr_reader :user_id, :credentials
5
+
6
+ # @param [Hash] credentials
7
+ # @option credentials [String] :user_id google username/email
8
+ # @option credentials [String] :password password for given username/email
9
+ def initialize(credentials)
10
+ if MultiXml.parser.to_s == "MultiXml::Parsers::Ox"
11
+ raise StandardError, "MultiXml parser is set to :ox - picasa gem will not work with it currently, use one of: :libxml, :nokogiri, :rexml"
12
+ end
13
+ @user_id = credentials.fetch(:user_id)
14
+ @credentials = credentials
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,30 @@
1
+ require "picasa/api/base"
2
+
3
+ module Picasa
4
+ module API
5
+ class Photo < Base
6
+ # Creates photo for given album
7
+ #
8
+ # @param [String] album_id album id
9
+ # @param [Hash] options request parameters
10
+ # @option options [String] :file_path path to photo file, rest of required attributes might be guessed based on file (i.e. "/home/john/Images/me.png")
11
+ # @option options [String] :title title of photo
12
+ # @option options [String] :summary summary of photo
13
+ # @option options [String] :binary binary data (i.e. File.open("my-photo.png", "rb").read)
14
+ # @option options [String] :content_type ["image/jpeg", "image/png", "image/bmp", "image/gif"] content type of given image
15
+ def create(album_id, params = {})
16
+ file = params[:file_path] ? File.new(params.delete(:file_path)) : nil
17
+ params[:boundary] ||= "===============PicasaRubyGem=="
18
+ params[:title] ||= (file && file.name) || raise(ArgumentError.new("title must be specified"))
19
+ params[:binary] ||= (file && file.binary) || raise(ArgumentError.new("binary must be specified"))
20
+ params[:content_type] ||= (file && file.content_type) || raise(ArgumentError.new("content_type must be specified"))
21
+ template = Template.new(:new_photo, params)
22
+ headers = {"Content-Type" => "multipart/related; boundary=\"#{params[:boundary]}\""}
23
+
24
+ uri = URI.parse("/data/feed/api/user/#{user_id}/albumid/#{album_id}")
25
+ parsed_body = Connection.new(credentials).post(uri.path, template.render, headers)
26
+ Presenter::Photo.new(parsed_body["entry"])
27
+ end
28
+ end
29
+ end
30
+ end
@@ -1,24 +1,13 @@
1
+ require "picasa/api/base"
2
+
1
3
  module Picasa
2
4
  module API
3
- class Tag
4
- attr_reader :user_id, :credentials
5
-
6
- # @param [Hash] credentials
7
- # @option credentials [String] :user_id google username/email
8
- # @option credentials [String] :password password for given username/email
9
- def initialize(credentials)
10
- if MultiXml.parser.to_s == "MultiXml::Parsers::Ox"
11
- raise StandardError, "MultiXml parser is set to :ox - picasa gem will not work with it currently, use one of: :libxml, :nokogiri, :rexml"
12
- end
13
- @user_id = credentials.fetch(:user_id)
14
- @credentials = credentials
15
- end
16
-
5
+ class Tag < Base
17
6
  # Returns tag list - when album_id is not specified, list of user tags will be returned
18
7
  #
19
8
  # @param [Hash] options additional options included in request
20
- # @option option [String] :album_id retrieve tags for given album
21
- # @option option [String] :photo_id retrieve tags for given photo (album_id must be provided)
9
+ # @option options [String] :album_id retrieve tags for given album
10
+ # @option options [String] :photo_id retrieve tags for given photo (album_id must be provided)
22
11
  #
23
12
  # @return [Presenter::TagList]
24
13
  def list(options = {})
data/lib/picasa/client.rb CHANGED
@@ -21,6 +21,17 @@ module Picasa
21
21
  API::Album.new(credentials)
22
22
  end
23
23
 
24
+ # @return [API::Photo]
25
+ #
26
+ # @example
27
+ # client = Picasa::Client.new(user_id: "my.email@google.com", password: "secret")
28
+ # photo = client.photo.create("album-id", title: "My picture", binary: "image-binary-data", content_type: "image/jpeg")
29
+ # photo.id
30
+ # # => "4322232322421"
31
+ def photo
32
+ API::Photo.new(credentials)
33
+ end
34
+
24
35
  # @return [API::Tag]
25
36
  #
26
37
  # @example
@@ -23,6 +23,23 @@ module Picasa
23
23
 
24
24
  path = path_with_params(path, params)
25
25
  request = Net::HTTP::Get.new(path, headers)
26
+ response = handle_response(http.request(request))
27
+ MultiXml.parse(response.body)
28
+ end
29
+
30
+ def post(path, body, custom_headers = {})
31
+ authenticate if auth?
32
+
33
+ request = Net::HTTP::Post.new(path, headers.merge(custom_headers))
34
+ request.body = body
35
+ response = handle_response(http.request(request))
36
+ MultiXml.parse(response.body)
37
+ end
38
+
39
+ def delete(path, custom_headers = {})
40
+ authenticate if auth?
41
+
42
+ request = Net::HTTP::Delete.new(path, headers.merge(custom_headers))
26
43
  handle_response(http.request(request))
27
44
  end
28
45
 
@@ -43,16 +60,24 @@ module Picasa
43
60
  def handle_response(response)
44
61
  case response.code.to_i
45
62
  when 200...300
46
- MultiXml.parse(response.body)
63
+ response
64
+ when 403
65
+ raise ForbiddenError.new(response.body, response)
47
66
  when 404
48
67
  raise NotFoundError.new(response.body, response)
68
+ when 412
69
+ raise PreconditionFailedError.new(response.body, response)
49
70
  else
50
- raise ResponseError.new(reponse.body, response)
71
+ raise ResponseError.new(response.body, response)
51
72
  end
52
73
  end
53
74
 
54
75
  def headers
55
- {"User-Agent" => "ruby-gem-v#{Picasa::VERSION}", "GData-Version" => API_VERSION}.tap do |headers|
76
+ {
77
+ "User-Agent" => client_name,
78
+ "GData-Version" => API_VERSION,
79
+ "Content-Type" => "application/atom+xml"
80
+ }.tap do |headers|
56
81
  headers["Authorization"] = "GoogleLogin auth=#{@auth_key}" if @auth_key
57
82
  end
58
83
  end
@@ -61,23 +86,14 @@ module Picasa
61
86
  !password.nil?
62
87
  end
63
88
 
64
- def validate_email!
65
- unless user_id =~ /[a-z0-9][a-z0-9._%+-]+[a-z0-9]@[a-z0-9][a-z0-9.-][a-z0-9]+\.[a-z]{2,6}/i
66
- raise ArgumentError.new("user_id must be a valid E-mail address when authentication is used.")
67
- end
68
- end
69
-
70
89
  def authenticate
71
- validate_email!
72
-
73
90
  data = inline_params({"accountType" => "HOSTED_OR_GOOGLE",
74
91
  "Email" => user_id,
75
92
  "Passwd" => password,
76
93
  "service" => "lh2",
77
- "source" => "ruby-gem-v#{Picasa::VERSION}"})
94
+ "source" => client_name})
78
95
 
79
- response = http(API_AUTH_URL).post("/accounts/ClientLogin", data)
80
- raise ResponseError.new(response.body, response) unless response.is_a? Net::HTTPSuccess
96
+ response = handle_response(http(API_AUTH_URL).post("/accounts/ClientLogin", data))
81
97
 
82
98
  @auth_key = extract_auth_key(response.body)
83
99
  end
@@ -87,5 +103,9 @@ module Picasa
87
103
  params = Hash[response]
88
104
  params["Auth"]
89
105
  end
106
+
107
+ def client_name
108
+ "ruby-gem-v#{Picasa::VERSION}"
109
+ end
90
110
  end
91
111
  end
@@ -16,4 +16,6 @@ module Picasa
16
16
  end
17
17
 
18
18
  class NotFoundError < ResponseError; end
19
+ class ForbiddenError < ResponseError; end
20
+ class PreconditionFailedError < ResponseError; end
19
21
  end
@@ -0,0 +1,36 @@
1
+ module Picasa
2
+ class File
3
+ attr_reader :path
4
+
5
+ def initialize(path)
6
+ @path = path || raise(ArgumentError.new("path not specified"))
7
+ end
8
+
9
+ def name
10
+ @name ||= ::File.basename(path, ".*")
11
+ end
12
+
13
+ def extension
14
+ @extension ||= ::File.extname(path)[1..-1]
15
+ end
16
+
17
+ def binary
18
+ @binary ||= ::File.open(path, "rb").read
19
+ end
20
+
21
+ def content_type
22
+ @content_type ||= case extension
23
+ when /^jpe?g$/i
24
+ "image/jpeg"
25
+ when /^gif$/i
26
+ "image/gif"
27
+ when /^png$/i
28
+ "image/png"
29
+ when /^bmp$/i
30
+ "image/bmp"
31
+ else
32
+ raise StandarError.new("Content type cannot be guessed from file extension: #{extension}")
33
+ end
34
+ end
35
+ end
36
+ end
@@ -16,7 +16,7 @@ module Picasa
16
16
 
17
17
  # @return [Array<Presenter::Link>]
18
18
  def links
19
- @links ||= safe_retrieve(parsed_body, "link").map { |link| Link.new(link) }
19
+ @links ||= array_wrap(safe_retrieve(parsed_body, "link")).map { |link| Link.new(link) }
20
20
  end
21
21
 
22
22
  # @return [Presenter::Media]
@@ -34,6 +34,11 @@ module Picasa
34
34
  @updated ||= map_to_date(safe_retrieve(parsed_body, "updated"))
35
35
  end
36
36
 
37
+ # @return [String]
38
+ def etag
39
+ @etag ||= safe_retrieve(parsed_body, "etag")
40
+ end
41
+
37
42
  # @return [String]
38
43
  def title
39
44
  @title ||= safe_retrieve(parsed_body, "title")
@@ -88,6 +93,16 @@ module Picasa
88
93
  def nickname
89
94
  @nickname ||= safe_retrieve(parsed_body, "nickname")
90
95
  end
96
+
97
+ # @return [true, false, nil]
98
+ def allow_prints
99
+ @allow_prints ||= map_to_boolean(safe_retrieve(parsed_body, "allowPrints"))
100
+ end
101
+
102
+ # @return [true, false, nil]
103
+ def allow_downloads
104
+ @allow_downloads ||= map_to_boolean(safe_retrieve(parsed_body, "allowDownloads"))
105
+ end
91
106
  end
92
107
  end
93
108
  end
@@ -16,7 +16,7 @@ module Picasa
16
16
 
17
17
  # @return [Array<Presenter::Link>]
18
18
  def links
19
- @links ||= safe_retrieve(parsed_body, "link").map { |link| Link.new(link) }
19
+ @links ||= array_wrap(safe_retrieve(parsed_body, "link")).map { |link| Link.new(link) }
20
20
  end
21
21
 
22
22
  # @return [String]
@@ -23,6 +23,11 @@ module Picasa
23
23
  @id ||= array_wrap(safe_retrieve(parsed_body, "id"))[1]
24
24
  end
25
25
 
26
+ # @return [String]
27
+ def etag
28
+ @etag ||= safe_retrieve(parsed_body, "etag")
29
+ end
30
+
26
31
  # @return [DateTime]
27
32
  def published
28
33
  @published ||= map_to_date(safe_retrieve(parsed_body, "published"))
@@ -10,7 +10,7 @@ module Picasa
10
10
 
11
11
  # @return [Array<Presenter::Link>]
12
12
  def links
13
- @links ||= safe_retrieve(parsed_body, "link").map { |link| Link.new(link) }
13
+ @links ||= array_wrap(safe_retrieve(parsed_body, "link")).map { |link| Link.new(link) }
14
14
  end
15
15
 
16
16
  # @return [DateTime]
@@ -23,6 +23,11 @@ module Picasa
23
23
  @title ||= safe_retrieve(parsed_body, "title")
24
24
  end
25
25
 
26
+ # @return [String]
27
+ def etag
28
+ @etag ||= safe_retrieve(parsed_body, "etag")
29
+ end
30
+
26
31
  # @return [String]
27
32
  def summary
28
33
  @summary ||= safe_retrieve(parsed_body, "summary")
@@ -16,7 +16,7 @@ module Picasa
16
16
 
17
17
  # @return [Array<Presenter::Link>]
18
18
  def links
19
- @links ||= safe_retrieve(parsed_body, "link").map { |link| Link.new(link) }
19
+ @links ||= array_wrap(safe_retrieve(parsed_body, "link")).map { |link| Link.new(link) }
20
20
  end
21
21
 
22
22
  # @return [String]
@@ -0,0 +1,25 @@
1
+ require "ostruct"
2
+ require "erb"
3
+
4
+ module Picasa
5
+ class Template
6
+ attr_reader :name, :params
7
+
8
+ def initialize(name, params)
9
+ @name = name
10
+ @params = params
11
+ end
12
+
13
+ def file
14
+ @file ||= IO.read(::File.expand_path("../templates/#{name}.xml.erb", __FILE__))
15
+ end
16
+
17
+ def struct
18
+ @struct ||= OpenStruct.new(params)
19
+ end
20
+
21
+ def render
22
+ ERB.new(file).result(struct.instance_eval { binding })
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,19 @@
1
+ <entry xmlns="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" xmlns:gphoto="http://schemas.google.com/photos/2007">
2
+ <title type="text"><%= title %></title>
3
+ <% if summary %>
4
+ <summary type="text"><%= summary %></summary>
5
+ <% end %>
6
+ <% if location %>
7
+ <gphoto:location><%= location %></gphoto:location>
8
+ <% end %>
9
+ <% if access %>
10
+ <gphoto:access><%= access %></gphoto:access>
11
+ <% end %>
12
+ <gphoto:timestamp><%= timestamp %></gphoto:timestamp>
13
+ <% if keywords %>
14
+ <media:group>
15
+ <media:keywords><%= keywords %></media:keywords>
16
+ </media:group>
17
+ <% end %>
18
+ <category scheme="http://schemas.google.com/g/2005#kind" term="http://schemas.google.com/photos/2007#album"></category>
19
+ </entry>
@@ -0,0 +1,15 @@
1
+ --<%= boundary %>
2
+ Content-type: application/atom+xml
3
+
4
+ <entry xmlns="http://www.w3.org/2005/Atom">
5
+ <title><%= title %></title>
6
+ <% if summary %>
7
+ <summary><%= summary %></summary>
8
+ <% end %>
9
+ <category scheme="http://schemas.google.com/g/2005#kind" term="http://schemas.google.com/photos/2007#photo"/>
10
+ </entry>
11
+ --<%= boundary %>
12
+ Content-type: <%= content_type %>
13
+
14
+ <%= binary %>
15
+ --<%= boundary %>
data/lib/picasa/utils.rb CHANGED
@@ -12,7 +12,7 @@ module Picasa
12
12
  end
13
13
  end
14
14
 
15
- # Ported from ActiveSupport
15
+ # Ported from activesupport gem
16
16
  def array_wrap(object)
17
17
  if object.nil?
18
18
  []
@@ -1,3 +1,3 @@
1
1
  module Picasa
2
- VERSION = "0.4.2"
2
+ VERSION = "0.5.0"
3
3
  end
@@ -21,4 +21,26 @@ describe Picasa::API::Album do
21
21
  assert_equal "Wojciech Wnętrzak", album_show.author.name
22
22
  end
23
23
  end
24
+
25
+ describe "#create" do
26
+ it "gives correct parsed body fragment" do
27
+ stub_request(:post, "https://www.google.com/accounts/ClientLogin").to_return(fixture("auth/success.txt"))
28
+ stub_request(:post, "https://picasaweb.google.com/data/feed/api/user/w.wnetrzak@gmail.com").to_return(fixture("album/album-create.txt"))
29
+
30
+ album_show = Picasa::API::Album.new(:user_id => "w.wnetrzak@gmail.com", :password => "secret").create(:title => "album")
31
+
32
+ assert_equal "Wojciech Wnętrzak", album_show.author.name
33
+ end
34
+ end
35
+
36
+ describe "#destroy" do
37
+ it "gives true when success" do
38
+ stub_request(:post, "https://www.google.com/accounts/ClientLogin").to_return(fixture("auth/success.txt"))
39
+ stub_request(:delete, "https://picasaweb.google.com/data/entry/api/user/w.wnetrzak@gmail.com/albumid/123").to_return(:status => 200, :body => "")
40
+
41
+ result = Picasa::API::Album.new(:user_id => "w.wnetrzak@gmail.com", :password => "secret").destroy("123")
42
+
43
+ assert_equal true, result
44
+ end
45
+ end
24
46
  end
@@ -0,0 +1,46 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require "helper"
3
+
4
+ describe Picasa::API::Photo do
5
+ describe "#create" do
6
+ it "gives correct parsed body fragment" do
7
+ stub_request(:post, "https://www.google.com/accounts/ClientLogin").to_return(fixture("auth/success.txt"))
8
+ stub_request(:post, "https://picasaweb.google.com/data/feed/api/user/w.wnetrzak@gmail.com/albumid/123").to_return(fixture("photo/photo-created.txt"))
9
+
10
+ photo = Picasa::API::Photo.new(:user_id => "w.wnetrzak@gmail.com", :password => "secret")
11
+ photo_create = photo.create("123", :title => "test", :binary => "binary", :content_type => "image/png")
12
+
13
+ assert_equal 27040, photo_create.size
14
+ end
15
+
16
+ it "raises ArgumentError when no title" do
17
+ photo = Picasa::API::Photo.new(:user_id => "w.wnetrzak@gmail.com", :password => "secret")
18
+ assert_raises Picasa::ArgumentError, /title/ do
19
+ photo.create("123", :binary => "binary", :content_type => "image/png")
20
+ end
21
+ end
22
+
23
+ it "raises ArgumentError when no binary" do
24
+ photo = Picasa::API::Photo.new(:user_id => "w.wnetrzak@gmail.com", :password => "secret")
25
+ assert_raises Picasa::ArgumentError, /binary/ do
26
+ photo.create("123", :title => "test", :content_type => "image/png")
27
+ end
28
+ end
29
+
30
+ it "raises ArgumentError when no content type" do
31
+ photo = Picasa::API::Photo.new(:user_id => "w.wnetrzak@gmail.com", :password => "secret")
32
+ assert_raises Picasa::ArgumentError, /content_type/ do
33
+ photo.create("123", :title => "test", :binary => "binary")
34
+ end
35
+ end
36
+
37
+ it "guesses required attributes from file path" do
38
+ stub_request(:post, "https://www.google.com/accounts/ClientLogin").to_return(fixture("auth/success.txt"))
39
+ stub_request(:post, "https://picasaweb.google.com/data/feed/api/user/w.wnetrzak@gmail.com/albumid/123").to_return(fixture("photo/photo-created.txt"))
40
+
41
+ photo = Picasa::API::Photo.new(:user_id => "w.wnetrzak@gmail.com", :password => "secret")
42
+
43
+ assert photo.create("123", :file_path => image_path("lena.jpg"))
44
+ end
45
+ end
46
+ end
@@ -41,13 +41,25 @@ describe Picasa::Connection do
41
41
  connection = Picasa::Connection.new(:user_id => "john.doe@domain.com")
42
42
  uri = URI.parse("/data/feed/api/user/#{connection.user_id}/albumid/non-existing")
43
43
 
44
- stub_request(:get, "https://picasaweb.google.com" + uri.path).to_return(fixture("not_found.txt"))
44
+ stub_request(:get, "https://picasaweb.google.com" + uri.path).to_return(fixture("exceptions/not_found.txt"))
45
45
 
46
46
  assert_raises Picasa::NotFoundError, "Invalid entity id: non-existing" do
47
47
  connection.get(uri.path)
48
48
  end
49
49
  end
50
50
 
51
+ it "raises PreconditionFailed exception when 412 returned" do
52
+ connection = Picasa::Connection.new(:user_id => "john.doe@domain.com", :password => "secret")
53
+ uri = URI.parse("/data/feed/api/user/#{connection.user_id}/albumid/123")
54
+
55
+ stub_request(:post, "https://www.google.com/accounts/ClientLogin").to_return(fixture("auth/success.txt"))
56
+ stub_request(:delete, "https://picasaweb.google.com" + uri.path).to_return(fixture("exceptions/precondition_failed.txt"))
57
+
58
+ assert_raises Picasa::PreconditionFailedError, "Mismatch: etags = [oldetag], version = [7]" do
59
+ connection.delete(uri.path, {"If-Match" => "oldetag"})
60
+ end
61
+ end
62
+
51
63
  describe "authentication" do
52
64
  it "successfully authenticates" do
53
65
  connection = Picasa::Connection.new(:user_id => "john.doe@domain.com", :password => "secret")
@@ -60,21 +72,13 @@ describe Picasa::Connection do
60
72
  refute_nil connection.get(uri.path)
61
73
  end
62
74
 
63
- it "raises ArgumentError when invalid email given" do
64
- connection = Picasa::Connection.new(:user_id => "john.doe", :password => "secret")
65
-
66
- assert_raises(Picasa::ArgumentError) do
67
- connection.get("/")
68
- end
69
- end
70
-
71
75
  it "raises an ResponseError when authentication failed" do
72
76
  connection = Picasa::Connection.new(:user_id => "john.doe@domain.com", :password => "secret")
73
77
  uri = URI.parse("/data/feed/api/user/#{connection.user_id}")
74
78
 
75
- stub_request(:post, "https://www.google.com/accounts/ClientLogin").to_return(fixture("auth/failure.txt"))
79
+ stub_request(:post, "https://www.google.com/accounts/ClientLogin").to_return(fixture("exceptions/forbidden.txt"))
76
80
 
77
- assert_raises(Picasa::ResponseError) do
81
+ assert_raises(Picasa::ForbiddenError) do
78
82
  connection.get(uri.path)
79
83
  end
80
84
  end
data/test/file_test.rb ADDED
@@ -0,0 +1,29 @@
1
+ require "helper"
2
+
3
+ describe Picasa::File do
4
+ it "raises argument error when nil path given" do
5
+ assert_raises Picasa::ArgumentError do
6
+ Picasa::File.new(nil)
7
+ end
8
+ end
9
+
10
+ it "it returns file name" do
11
+ file = Picasa::File.new(image_path("lena.jpg"))
12
+ assert_equal "lena", file.name
13
+ end
14
+
15
+ it "it returns file extension" do
16
+ file = Picasa::File.new(image_path("lena.jpg"))
17
+ assert_equal "jpg", file.extension
18
+ end
19
+
20
+ it "it guesses content type" do
21
+ file = Picasa::File.new(image_path("lena.jpg"))
22
+ assert_equal "image/jpeg", file.content_type
23
+ end
24
+
25
+ it "returns binary read file" do
26
+ file = Picasa::File.new(image_path("lena.jpg"))
27
+ assert_equal String, file.binary.class
28
+ end
29
+ end
@@ -0,0 +1,21 @@
1
+ HTTP/1.1 201 Created
2
+ Gdata-version: 2.0
3
+ Content-length: 2988
4
+ Via: HTTP/1.1 GWA
5
+ Content-location: https://picasaweb.google.com/data/entry/api/user/106136347770555028022/albumid/5783214424453412737
6
+ X-content-type-options: nosniff
7
+ Etag: "YD0qeyI."
8
+ Set-cookie: _rtok=v2BIV10GEHe2; Path=/; Secure; HttpOnly, S=photos_html=TyVjxRd5Em09qzhPl6Kjsw; Domain=.google.com; Path=/; Secure; HttpOnly
9
+ Expires: Sat, 01 Sep 2012 14:25:36 GMT
10
+ Vary: Accept, X-GData-Authorization, GData-Version
11
+ X-google-cache-control: remote-fetch
12
+ Server: GSE
13
+ X-xss-protection: 1; mode=block
14
+ Location: https://picasaweb.google.com/data/entry/api/user/106136347770555028022/albumid/5783214424453412737
15
+ Cache-control: private, max-age=0, must-revalidate, no-transform
16
+ Date: Sat, 01 Sep 2012 14:25:36 GMT
17
+ X-frame-options: SAMEORIGIN
18
+ Content-type: application/atom+xml; charset=UTF-8; type=entry
19
+ -content-encoding: gzip
20
+
21
+ <?xml version='1.0' encoding='UTF-8'?><entry xmlns='http://www.w3.org/2005/Atom' xmlns:app='http://www.w3.org/2007/app' xmlns:gphoto='http://schemas.google.com/photos/2007' xmlns:media='http://search.yahoo.com/mrss/' xmlns:gd='http://schemas.google.com/g/2005' xmlns:gml='http://www.opengis.net/gml' xmlns:georss='http://www.georss.org/georss' gd:etag='&quot;YD0qeyI.&quot;'><id>https://picasaweb.google.com/data/entry/user/106136347770555028022/albumid/5783214424453412737</id><published>1970-01-16T14:01:49.000Z</published><updated>2012-09-01T14:25:36.447Z</updated><app:edited>2012-09-01T14:25:36.447Z</app:edited><category scheme='http://schemas.google.com/g/2005#kind' term='http://schemas.google.com/photos/2007#album'/><title>Trip To Poland</title><summary>This was the recent trip I took to Poland.</summary><rights>protected</rights><link rel='http://schemas.google.com/g/2005#feed' type='application/atom+xml' href='https://picasaweb.google.com/data/feed/api/user/106136347770555028022/albumid/5783214424453412737'/><link rel='alternate' type='text/html' href='https://picasaweb.google.com/106136347770555028022/TripToPoland'/><link rel='self' type='application/atom+xml' href='https://picasaweb.google.com/data/entry/api/user/106136347770555028022/albumid/5783214424453412737'/><link rel='edit' type='application/atom+xml' href='https://picasaweb.google.com/data/entry/api/user/106136347770555028022/albumid/5783214424453412737'/><link rel='http://schemas.google.com/acl/2007#accessControlList' type='application/atom+xml' href='https://picasaweb.google.com/data/entry/api/user/106136347770555028022/albumid/5783214424453412737/acl'/><author><name>Wojciech Wnętrzak</name><uri>https://picasaweb.google.com/106136347770555028022</uri></author><gphoto:id>5783214424453412737</gphoto:id><gphoto:name>TripToPoland</gphoto:name><gphoto:location>Poland</gphoto:location><gphoto:access>protected</gphoto:access><gphoto:timestamp>1346509000</gphoto:timestamp><gphoto:numphotos>0</gphoto:numphotos><gphoto:numphotosremaining>1000</gphoto:numphotosremaining><gphoto:bytesUsed>0</gphoto:bytesUsed><gphoto:user>106136347770555028022</gphoto:user><gphoto:nickname>Wojciech Wnętrzak</gphoto:nickname><media:group><media:content url='https://lh3.googleusercontent.com/-8od2ksbQgT0/UEIa4NV194E/AAAAAAAAAqI/XIYGNrflH9A/TripToPoland.jpg' type='image/jpeg' medium='image'/><media:credit>Wojciech Wnętrzak</media:credit><media:description type='plain'>This was the recent trip I took to Poland.</media:description><media:keywords/><media:thumbnail url='https://lh3.googleusercontent.com/-8od2ksbQgT0/UEIa4NV194E/AAAAAAAAAqI/XIYGNrflH9A/s160-c/TripToPoland.jpg' height='160' width='160'/><media:title type='plain'>Trip To Poland</media:title></media:group><georss:where><gml:Envelope><gml:lowerCorner>49.0025444 14.1336215</gml:lowerCorner><gml:upperCorner>54.8363315 24.1566505</gml:upperCorner></gml:Envelope><gml:Point><gml:pos>51.919438 19.145136</gml:pos></gml:Point></georss:where></entry>
@@ -0,0 +1,14 @@
1
+ HTTP/1.1 412 Precondition Failed
2
+ Expires: Sat, 01 Sep 2012 17:21:46 GMT
3
+ Date: Sat, 01 Sep 2012 17:21:46 GMT
4
+ Cache-Control: private, max-age=0, must-revalidate
5
+ Set-Cookie: _rtok=3ODY353H4sXQ; Path=/; Secure; HttpOnly
6
+ Set-Cookie: S=photos_html=kOBZzBdZTgXA12OVzxVTbg; Domain=.google.com; Path=/; Secure; HttpOnly
7
+ Content-Type: text/html; charset=UTF-8
8
+ X-Content-Type-Options: nosniff
9
+ X-Frame-Options: SAMEORIGIN
10
+ X-XSS-Protection: 1; mode=block
11
+ Server: GSE
12
+ Transfer-Encoding: chunked
13
+
14
+ Mismatch: etags = [oldetag], version = [7]
Binary file
@@ -0,0 +1,21 @@
1
+ HTTP/1.1 201 Created
2
+ Gdata-version: 2.0
3
+ Content-length: 3698
4
+ Via: HTTP/1.1 GWA
5
+ Content-location: https://picasaweb.google.com/data/entry/api/user/106136347770555028022/albumid/5782912491601845665/photoid/5782919030689216834
6
+ X-content-type-options: nosniff
7
+ Etag: "YD0qeyI."
8
+ Set-cookie: _rtok=RJK5U_sWX0B8; Path=/; Secure; HttpOnly, S=photos_html=1MH7BnwEbzDbPVcrJGKtlw; Domain=.google.com; Path=/; Secure; HttpOnly
9
+ Expires: Fri, 31 Aug 2012 19:19:21 GMT
10
+ Vary: Accept, X-GData-Authorization, GData-Version
11
+ X-google-cache-control: remote-fetch
12
+ Server: GSE
13
+ X-xss-protection: 1; mode=block
14
+ Location: https://picasaweb.google.com/data/entry/api/user/106136347770555028022/albumid/5782912491601845665/photoid/5782919030689216834
15
+ Cache-control: private, max-age=0, must-revalidate, no-transform
16
+ Date: Fri, 31 Aug 2012 19:19:21 GMT
17
+ X-frame-options: SAMEORIGIN
18
+ Content-type: application/atom+xml; charset=UTF-8; type=entry
19
+ -content-encoding: gzip
20
+
21
+ <?xml version='1.0' encoding='UTF-8'?><entry xmlns='http://www.w3.org/2005/Atom' xmlns:exif='http://schemas.google.com/photos/exif/2007' xmlns:app='http://www.w3.org/2007/app' xmlns:gphoto='http://schemas.google.com/photos/2007' xmlns:media='http://search.yahoo.com/mrss/' xmlns:gd='http://schemas.google.com/g/2005' gd:etag='&quot;YD0qeyI.&quot;'><id>https://picasaweb.google.com/data/entry/user/106136347770555028022/albumid/5782912491601845665/photoid/5782919030689216834</id><published>2012-08-31T19:19:20.000Z</published><updated>2012-08-31T19:19:20.882Z</updated><app:edited>2012-08-31T19:19:20.882Z</app:edited><category scheme='http://schemas.google.com/g/2005#kind' term='http://schemas.google.com/photos/2007#photo'/><title>ancient-test.jpg</title><summary>sent from playground</summary><content type='image/jpeg' src='https://lh5.googleusercontent.com/-HawhXkeW2Y4/UEEOOB0TuUI/AAAAAAAAAos/V1lCZ7oNAEg/ancient-test.jpg'/><link rel='http://schemas.google.com/g/2005#feed' type='application/atom+xml' href='https://picasaweb.google.com/data/feed/api/user/106136347770555028022/albumid/5782912491601845665/photoid/5782919030689216834'/><link rel='alternate' type='text/html' href='https://picasaweb.google.com/106136347770555028022/PrivateTesting#5782919030689216834'/><link rel='http://schemas.google.com/photos/2007#canonical' type='text/html' href='https://picasaweb.google.com/lh/photo/HerZrwYwEPv8XgInPPde-NMTjNZETYmyPJy0liipFm0'/><link rel='self' type='application/atom+xml' href='https://picasaweb.google.com/data/entry/api/user/106136347770555028022/albumid/5782912491601845665/photoid/5782919030689216834'/><link rel='edit' type='application/atom+xml' href='https://picasaweb.google.com/data/entry/api/user/106136347770555028022/albumid/5782912491601845665/photoid/5782919030689216834'/><link rel='edit-media' type='image/jpeg' href='https://picasaweb.google.com/data/media/api/user/106136347770555028022/albumid/5782912491601845665/photoid/5782919030689216834'/><link rel='http://schemas.google.com/photos/2007#report' type='text/html' href='https://picasaweb.google.com/lh/reportAbuse?uname=106136347770555028022&amp;aid=5782912491601845665&amp;iid=5782919030689216834'/><gphoto:id>5782919030689216834</gphoto:id><gphoto:albumid>5782912491601845665</gphoto:albumid><gphoto:access>only_you</gphoto:access><gphoto:width>473</gphoto:width><gphoto:height>506</gphoto:height><gphoto:size>27040</gphoto:size><gphoto:checksum/><gphoto:timestamp>1346440760000</gphoto:timestamp><gphoto:imageVersion>651</gphoto:imageVersion><gphoto:commentingEnabled>true</gphoto:commentingEnabled><gphoto:commentCount>0</gphoto:commentCount><gphoto:license id='0' name='Wszelkie prawa zastrzeżone' url=''>ALL_RIGHTS_RESERVED</gphoto:license><exif:tags><exif:imageUniqueID>6b5722298561a74e1476868e2ae28b7d</exif:imageUniqueID></exif:tags><media:group><media:content url='https://lh5.googleusercontent.com/-HawhXkeW2Y4/UEEOOB0TuUI/AAAAAAAAAos/V1lCZ7oNAEg/ancient-test.jpg' height='506' width='473' type='image/jpeg' medium='image'/><media:credit>Wojciech Wnętrzak</media:credit><media:description type='plain'>sent from playground</media:description><media:keywords/><media:thumbnail url='https://lh5.googleusercontent.com/-HawhXkeW2Y4/UEEOOB0TuUI/AAAAAAAAAos/V1lCZ7oNAEg/s72/ancient-test.jpg' height='72' width='68'/><media:thumbnail url='https://lh5.googleusercontent.com/-HawhXkeW2Y4/UEEOOB0TuUI/AAAAAAAAAos/V1lCZ7oNAEg/s144/ancient-test.jpg' height='144' width='135'/><media:thumbnail url='https://lh5.googleusercontent.com/-HawhXkeW2Y4/UEEOOB0TuUI/AAAAAAAAAos/V1lCZ7oNAEg/s288/ancient-test.jpg' height='288' width='270'/><media:title type='plain'>ancient-test.jpg</media:title></media:group></entry>
data/test/helper.rb CHANGED
@@ -14,6 +14,10 @@ class MiniTest::Unit::TestCase
14
14
  WebMock.disable_net_connect!
15
15
  end
16
16
 
17
+ def image_path(filename)
18
+ ::File.join("test", "fixtures", filename)
19
+ end
20
+
17
21
  # Recording response is as simple as writing in terminal:
18
22
  # curl -is -H "GData-Version: 2" "https://picasaweb.google.com/data/feed/api/user/username?prettyprint=true" -X GET > test/fixtures/albums.txt
19
23
  def fixture(filename)
@@ -20,6 +20,10 @@ describe Picasa::Presenter::Album do
20
20
  assert_equal 3, @album.links.size
21
21
  end
22
22
 
23
+ it "has etag" do
24
+ assert_equal "\"YDkqeyI.\"", @album.etag
25
+ end
26
+
23
27
  it "has media credit" do
24
28
  assert_equal "Wojciech Wnętrzak", @album.media.credit
25
29
  end
@@ -127,6 +131,10 @@ describe Picasa::Presenter::Album do
127
131
  assert_equal "5243667126168669553", @album.id
128
132
  end
129
133
 
134
+ it "has etag" do
135
+ assert_equal "W/\"DUICQX0_fSp7ImA9WhdSGEo.\"", @album.etag
136
+ end
137
+
130
138
  it "has name" do
131
139
  assert_equal "Test2", @album.name
132
140
  end
@@ -158,6 +166,14 @@ describe Picasa::Presenter::Album do
158
166
  it "has photo entries" do
159
167
  assert_equal 3, @album.entries.size
160
168
  end
169
+
170
+ it "has allow_prints" do
171
+ assert_equal true, @album.allow_prints
172
+ end
173
+
174
+ it "has allow_downloads" do
175
+ assert_equal true, @album.allow_downloads
176
+ end
161
177
  end
162
178
 
163
179
  describe "album from show with single photo" do
@@ -20,6 +20,10 @@ describe Picasa::Presenter::Photo do
20
20
  assert_equal "Wojciech Wnętrzak", @photo.media.credit
21
21
  end
22
22
 
23
+ it "has etag" do
24
+ assert_equal "\"YD4qeyI.\"", @photo.etag
25
+ end
26
+
23
27
  it "has id" do
24
28
  assert_equal "5243667226703402962", @photo.id
25
29
  end
@@ -24,6 +24,10 @@ describe Picasa::Presenter::Tag do
24
24
  assert_equal "2012-08-17T08:40:24+00:00", @tag.updated.to_s
25
25
  end
26
26
 
27
+ it "has etag" do
28
+ assert_equal "W/\"CE8GRXczfip7ImA9WhJWEUQ.\"", @tag.etag
29
+ end
30
+
27
31
  it "has title" do
28
32
  assert_equal "nice", @tag.title
29
33
  end
@@ -0,0 +1,25 @@
1
+ require "helper"
2
+
3
+ describe Picasa::Template do
4
+ it "has name" do
5
+ template = Picasa::Template.new(:new_album, {})
6
+ assert_equal :new_album, template.name
7
+ end
8
+
9
+ it "has params" do
10
+ template = Picasa::Template.new(:new_album, {:title => "My album"})
11
+ assert_equal({:title => "My album"}, template.params)
12
+ end
13
+
14
+ describe "new_album" do
15
+ it "renders title" do
16
+ template = Picasa::Template.new(:new_album, {:title => "My album"})
17
+ assert_match %q{<title type="text">My album</title>}, template.render
18
+ end
19
+
20
+ it "renders summary" do
21
+ template = Picasa::Template.new(:new_album, {:summary => "My summary"})
22
+ assert_match %q{<summary type="text">My summary</summary>}, template.render
23
+ end
24
+ end
25
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: picasa
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.2
4
+ version: 0.5.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-08-24 00:00:00.000000000 Z
12
+ date: 2012-09-15 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: multi_xml
@@ -72,12 +72,16 @@ files:
72
72
  - LICENSE
73
73
  - README.md
74
74
  - Rakefile
75
+ - extra/Thorfile
75
76
  - lib/picasa.rb
76
77
  - lib/picasa/api/album.rb
78
+ - lib/picasa/api/base.rb
79
+ - lib/picasa/api/photo.rb
77
80
  - lib/picasa/api/tag.rb
78
81
  - lib/picasa/client.rb
79
82
  - lib/picasa/connection.rb
80
83
  - lib/picasa/exceptions.rb
84
+ - lib/picasa/file.rb
81
85
  - lib/picasa/presenter/album.rb
82
86
  - lib/picasa/presenter/album_list.rb
83
87
  - lib/picasa/presenter/author.rb
@@ -89,23 +93,32 @@ files:
89
93
  - lib/picasa/presenter/tag.rb
90
94
  - lib/picasa/presenter/tag_list.rb
91
95
  - lib/picasa/presenter/thumbnail.rb
96
+ - lib/picasa/template.rb
97
+ - lib/picasa/templates/new_album.xml.erb
98
+ - lib/picasa/templates/new_photo.xml.erb
92
99
  - lib/picasa/utils.rb
93
100
  - lib/picasa/version.rb
94
101
  - picasa.gemspec
95
102
  - test/api/album_test.rb
103
+ - test/api/photo_test.rb
96
104
  - test/api/tag_test.rb
97
105
  - test/client_test.rb
98
106
  - test/connection_test.rb
107
+ - test/file_test.rb
108
+ - test/fixtures/album/album-create.txt
99
109
  - test/fixtures/album/album-list-with-tag.txt
100
110
  - test/fixtures/album/album-list.txt
101
111
  - test/fixtures/album/album-show-with-max-results.txt
102
112
  - test/fixtures/album/album-show-with-tag-and-many-photos.txt
103
113
  - test/fixtures/album/album-show-with-tag-and-one-photo.txt
104
114
  - test/fixtures/album/album-show.txt
105
- - test/fixtures/auth/failure.txt
106
115
  - test/fixtures/auth/success.txt
116
+ - test/fixtures/exceptions/forbidden.txt
117
+ - test/fixtures/exceptions/not_found.txt
118
+ - test/fixtures/exceptions/precondition_failed.txt
107
119
  - test/fixtures/json.txt
108
- - test/fixtures/not_found.txt
120
+ - test/fixtures/lena.jpg
121
+ - test/fixtures/photo/photo-created.txt
109
122
  - test/fixtures/photo/photo-list-all-with-q.txt
110
123
  - test/fixtures/photo/photo-list-all.txt
111
124
  - test/fixtures/photo/photo-list-user.txt
@@ -126,6 +139,7 @@ files:
126
139
  - test/presenter/tag_list_test.rb
127
140
  - test/presenter/tag_test.rb
128
141
  - test/presenter/thumbnail_test.rb
142
+ - test/template_test.rb
129
143
  - test/utils_test.rb
130
144
  homepage: https://github.com/morgoth/picasa
131
145
  licenses: []