picasa 0.4.2 → 0.5.0

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/.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: []