useless-museum 1.2.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +3 -0
- data/.rbenv-version +1 -0
- data/Gemfile +2 -0
- data/RESEARCH.md +5 -0
- data/Rakefile +1 -0
- data/lib/useless/museum/image.rb +112 -0
- data/lib/useless/museum/version.rb +5 -0
- data/lib/useless/museum.rb +158 -0
- data/spec/documentation_spec.rb +43 -0
- data/spec/museum/image_spec.rb +95 -0
- data/spec/museum_spec.rb +150 -0
- data/spec/spec_helper.rb +38 -0
- data/spec/support/assets/muffin-milk-stripped.jpg +0 -0
- data/spec/support/assets/muffin-milk.gif +0 -0
- data/spec/support/assets/muffin-milk.jpg +0 -0
- data/spec/support/assets/muffin-milk.png +0 -0
- data/spec/support/assets/muffin-milk.tiff +0 -0
- data/spec/support/assets/muffin-milk.txt +1 -0
- data/useless-museum.gemspec +28 -0
- metadata +185 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 98f22e2e08471ec4ead397a73c79909897b0874a
|
4
|
+
data.tar.gz: 4e0d973edf0d8f32469f21d38fb55952bf4dee5e
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 95056d8612069499e3e89d68eea0bdf9a51e0e24940cbe8d2df632467c9fdabe5bcab6d5ea4539ab59072ae355962e5f34cedc3c9d5a4a6f0306788cf56f9190
|
7
|
+
data.tar.gz: 59fc975e3558874f1d79497ac36cd2734a6f5d2bd733124cddaa5ca44cbbb71cfa0e9ee30f95ea0663e35725aa4ea376a49502a631b494b07b57dd74e04b5b85
|
data/.gitignore
ADDED
data/.rbenv-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.0.0-p0
|
data/Gemfile
ADDED
data/RESEARCH.md
ADDED
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'bundler/gem_tasks'
|
@@ -0,0 +1,112 @@
|
|
1
|
+
require 'mini_magick'
|
2
|
+
require 'exifr'
|
3
|
+
|
4
|
+
module Useless
|
5
|
+
class Museum
|
6
|
+
# Museum::Image is a utility class that provides two types of
|
7
|
+
# functionality:
|
8
|
+
# * image resizing based upon the Useless::Musuem specifications
|
9
|
+
# * access to the following image metadata: latitude, longitude, shot_at
|
10
|
+
# It takes an IO object upon initialization. The resize methods return
|
11
|
+
# instances of MiniMagick::Image.
|
12
|
+
class Image
|
13
|
+
def initialize(raw_io)
|
14
|
+
@raw_io = raw_io
|
15
|
+
end
|
16
|
+
|
17
|
+
# MiniMagick::Image for the original image
|
18
|
+
def base
|
19
|
+
@base ||= minimagick_copy
|
20
|
+
end
|
21
|
+
|
22
|
+
# Only JPEGs are considered valid
|
23
|
+
def valid?
|
24
|
+
base and ['JPEG', 'TIFF', 'PNG', 'GIF'].include?(base['format'])
|
25
|
+
end
|
26
|
+
|
27
|
+
# A 'small' image's longest side is 100 pixels,
|
28
|
+
def small
|
29
|
+
@small ||= version('100x100')
|
30
|
+
end
|
31
|
+
|
32
|
+
# 'medium' is 500 pixels
|
33
|
+
def medium
|
34
|
+
@medium ||= version('500x500')
|
35
|
+
end
|
36
|
+
|
37
|
+
# and 'large' is 1024 pixels
|
38
|
+
def large
|
39
|
+
@large ||= version('1024x1024')
|
40
|
+
end
|
41
|
+
|
42
|
+
# Just grab latitude
|
43
|
+
def latitude
|
44
|
+
@latitude ||= exifr.gps.latitude if exifr and exifr.gps
|
45
|
+
end
|
46
|
+
|
47
|
+
# and longitude from EXIFR
|
48
|
+
def longitude
|
49
|
+
@longitude ||= exifr.gps.longitude if exifr and exifr.gps
|
50
|
+
end
|
51
|
+
|
52
|
+
# Shot at is just the date_time provide from EXIFR
|
53
|
+
def shot_at
|
54
|
+
@shot_at ||= exifr.date_time if exifr
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
# An EXIFR::JPEG instance, which provides access to the image metadata.
|
60
|
+
def exifr
|
61
|
+
return nil unless base
|
62
|
+
|
63
|
+
@exifr ||= case base['format']
|
64
|
+
when 'JPEG' then EXIFR::JPEG.new(StringIO.new(blob))
|
65
|
+
when 'TIFF' then EXIFR::TIFF.new(StringIO.new(blob))
|
66
|
+
else nil
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def version(dimensions)
|
71
|
+
# Copy the image if possible,
|
72
|
+
if image = minimagick_copy
|
73
|
+
|
74
|
+
# resize it to the specified dimensions
|
75
|
+
image.resize dimensions
|
76
|
+
|
77
|
+
# and return
|
78
|
+
image
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def minimagick_copy
|
83
|
+
# Just instantiate a new instance with the raw image blob.
|
84
|
+
MiniMagick::Image.read(blob)
|
85
|
+
|
86
|
+
# If the 'image' cannot be parsed,
|
87
|
+
rescue MiniMagick::Invalid
|
88
|
+
|
89
|
+
# just return nil
|
90
|
+
nil
|
91
|
+
end
|
92
|
+
|
93
|
+
# A binary blob of the original image
|
94
|
+
def blob
|
95
|
+
@blob ||= begin
|
96
|
+
# Make sure the pointer is up front,
|
97
|
+
@raw_io.rewind
|
98
|
+
|
99
|
+
# read the bytes,
|
100
|
+
blob = @raw_io.read
|
101
|
+
|
102
|
+
# make sure they're un-encoded (encodings can cause multiple bytes to
|
103
|
+
# be interpreted as one, which is no good for images),
|
104
|
+
blob.force_encoding('BINARY')
|
105
|
+
|
106
|
+
# and return
|
107
|
+
blob
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,158 @@
|
|
1
|
+
require 'sinatra/base'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
require 'useless/doc/server/sinatra'
|
5
|
+
require 'useless/rack/files'
|
6
|
+
|
7
|
+
module Useless
|
8
|
+
class Museum < Sinatra::Base
|
9
|
+
require 'useless/museum/image'
|
10
|
+
require 'useless/museum/version'
|
11
|
+
|
12
|
+
use Useless::Rack::Files
|
13
|
+
|
14
|
+
register Useless::Doc::Server::Sinatra
|
15
|
+
|
16
|
+
enable :raise_errors
|
17
|
+
disable :show_exceptions, :dump_errors
|
18
|
+
set :logging, nil
|
19
|
+
|
20
|
+
doc 'Museum' do
|
21
|
+
url 'http://museum.useless.io'
|
22
|
+
|
23
|
+
description <<-DESC
|
24
|
+
The Museum of Whatever
|
25
|
+
DESC
|
26
|
+
end
|
27
|
+
|
28
|
+
get '/' do
|
29
|
+
redirect 'http://museum.doc.useless.io'
|
30
|
+
end
|
31
|
+
|
32
|
+
doc.get '/photos/:id' do
|
33
|
+
description 'Retrieve information about a photo.'
|
34
|
+
authentication_required false
|
35
|
+
parameter 'id', 'The ID of the desired photo.', type: 'path'
|
36
|
+
|
37
|
+
response 404, 'A photo with the specified ID could not be found.'
|
38
|
+
response 200, 'The photo was returned successfully.' do
|
39
|
+
body do
|
40
|
+
content_type 'application/json'
|
41
|
+
attribute 'id', 'The ID of the photo.'
|
42
|
+
attribute 'file_urls',
|
43
|
+
['An object containing each of files of the photo\'s sizes. "small" has a',
|
44
|
+
'longest side of 100 pixels, "medium" has a longest side of 500 pixels and',
|
45
|
+
'"large" has a longest side of 1024 pixels.'].join("\n"), type: 'object'
|
46
|
+
attribute 'latitude', 'The latitude where the photo was shot.', type: 'number'
|
47
|
+
attribute 'longitude', 'The longitude where the photo was shot.', type: 'number'
|
48
|
+
attribute 'shot_at', 'The date and time when the photo was shot.'
|
49
|
+
attribute 'description', 'A description of the photo provided by the owner.'
|
50
|
+
attribute 'created_by', 'The user who created the photo.', type: 'object'
|
51
|
+
attribute 'created_at', 'When the photo was created.'
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
get '/photos/:id' do
|
57
|
+
return 404 unless BSON::ObjectId.legal?(params[:id])
|
58
|
+
|
59
|
+
id = BSON::ObjectId.from_string(params[:id])
|
60
|
+
if record = env['useless.mongo']['museum.photos'].find_one(id)
|
61
|
+
photo_json(record)
|
62
|
+
else
|
63
|
+
404
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
doc.post '/photos' do
|
68
|
+
description 'Create a new photo.'
|
69
|
+
authentication_required true
|
70
|
+
|
71
|
+
body do
|
72
|
+
content_type 'multipart/form-data'
|
73
|
+
attribute 'photo', 'The actual file for the photo.', type: 'file'
|
74
|
+
attribute 'latitude',
|
75
|
+
['The latitude where the photo was taken. Only required if it cannot be',
|
76
|
+
'extracted from the photo itself. If the information can be extracted,',
|
77
|
+
'this will be ignored.'].join("\n"),
|
78
|
+
required: false, type: 'number', default: 'value extracted from photo'
|
79
|
+
attribute 'longitude', 'The longitude where the photo was taken. Follows the same rules as latitude.',
|
80
|
+
required: false, type: 'number', default: 'value extracted from photo'
|
81
|
+
attribute 'shot_at', 'The time that the photo was taken. Follows the same rules as latitude and longitude.',
|
82
|
+
required: false, default: 'value extracted from photo'
|
83
|
+
attribute 'description', 'The description of the photo.', required: false, default: nil
|
84
|
+
end
|
85
|
+
|
86
|
+
response 201, 'The photo was created successfully.' do
|
87
|
+
header 'Location', 'The URL of the new photo.'
|
88
|
+
end
|
89
|
+
response 401, 'The request could not be authenticated.'
|
90
|
+
response 422, 'latitude, longitude or shot_at could not be extracted from the photo. ' +
|
91
|
+
'Retry with the latitude, longitude and shot_at attributes specified.'
|
92
|
+
response 422, 'Invalid image format. Format must be one of JPEG, TIFF, PNG or GIF.'
|
93
|
+
end
|
94
|
+
|
95
|
+
post '/photos' do
|
96
|
+
unless env['useless.user']
|
97
|
+
halt 401, 'The request could not be authenticated.'
|
98
|
+
end
|
99
|
+
|
100
|
+
unless params[:file]
|
101
|
+
halt 422, 'File is a required parameter.'
|
102
|
+
end
|
103
|
+
|
104
|
+
file = params[:file][:tempfile]
|
105
|
+
image = Image.new(file)
|
106
|
+
|
107
|
+
unless image.valid?
|
108
|
+
halt 422, 'Invalid image format. Format must be one of JPEG, TIFF, PNG or GIF.'
|
109
|
+
end
|
110
|
+
|
111
|
+
document = {}
|
112
|
+
document['latitude'] = image.latitude || params[:latitude]
|
113
|
+
document['longitude'] = image.longitude || params[:longitude]
|
114
|
+
document['shot_at'] = image.shot_at || (params[:shot_at] && Time.parse(params[:shot_at]))
|
115
|
+
|
116
|
+
unless document['latitude'] and document['longitude'] and document['shot_at']
|
117
|
+
halt 422, 'latitude, longitude or shot_at could not be extracted from the photo. ' +
|
118
|
+
'Retry with the latitude, longitude and shot_at attributes specified.'
|
119
|
+
end
|
120
|
+
|
121
|
+
document['file_url'] = {
|
122
|
+
'small' => create_image_url(image.small),
|
123
|
+
'medium' => create_image_url(image.medium),
|
124
|
+
'large' => create_image_url(image.large)
|
125
|
+
}
|
126
|
+
document['description'] = params[:description]
|
127
|
+
document['created_by_id'] = env['useless.user']['_id']
|
128
|
+
document['created_at'] = document['updated_at'] = Time.now
|
129
|
+
photo_id = env['useless.mongo']['museum.photos'] << document
|
130
|
+
|
131
|
+
status 201
|
132
|
+
headers 'Location' => "http://museum.useless.io/photos/#{photo_id}"
|
133
|
+
content_type 'application/json'
|
134
|
+
photo_json(document)
|
135
|
+
end
|
136
|
+
|
137
|
+
def photo_json(raw_record)
|
138
|
+
record = raw_record.dup
|
139
|
+
|
140
|
+
record['id'] = record.delete('_id').to_s
|
141
|
+
|
142
|
+
record['shot_at'] = raw_record['shot_at'].iso8601
|
143
|
+
record['created_at'] = raw_record['created_at'].iso8601
|
144
|
+
record['updated_at'] = raw_record['updated_at'].iso8601
|
145
|
+
|
146
|
+
created_by_id = record.delete('created_by_id')
|
147
|
+
created_by = env['useless.mongo']['users'].find_one(created_by_id)
|
148
|
+
record['created_by'] = {'id' => created_by_id.to_s, 'handle' => created_by['handle']}
|
149
|
+
|
150
|
+
record.to_json
|
151
|
+
end
|
152
|
+
|
153
|
+
def create_image_url(image)
|
154
|
+
id = env['useless.fs'].put(image.to_blob, content_type: image.mime_type)
|
155
|
+
"http://museum.useless.io/files/#{id}"
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
2
|
+
|
3
|
+
require 'useless'
|
4
|
+
require 'oj'
|
5
|
+
|
6
|
+
describe 'museum.useless.io Documentation' do
|
7
|
+
include Rack::Test::Methods
|
8
|
+
|
9
|
+
def app
|
10
|
+
Useless::Rack.new do
|
11
|
+
map 'museum' => Useless::Museum
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
describe 'OPTIONS /' do
|
16
|
+
it 'should return JSON for the full API documentation' do
|
17
|
+
options 'http://museum.useless.io/'
|
18
|
+
last_response.status.should == 200
|
19
|
+
doc = Oj.load(last_response.body)
|
20
|
+
doc['name'].should == 'Museum'
|
21
|
+
doc['url'].should == 'http://museum.useless.io'
|
22
|
+
doc['resources'].length.should == 2
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
describe 'OPTIONS /photos/:id' do
|
27
|
+
it 'should return JSON for the resource documentation' do
|
28
|
+
options 'http://museum.useless.io/photos/:id'
|
29
|
+
last_response.status.should == 200
|
30
|
+
doc = Oj.load(last_response.body)
|
31
|
+
doc['path'].should == '/photos/:id'
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
describe 'OPTIONS /photos' do
|
36
|
+
it 'should return JSON for the resource documentation' do
|
37
|
+
options 'http://museum.useless.io/photos'
|
38
|
+
last_response.status.should == 200
|
39
|
+
doc = Oj.load(last_response.body)
|
40
|
+
doc['path'].should == '/photos'
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../spec_helper'
|
2
|
+
|
3
|
+
describe Useless::Museum::Image do
|
4
|
+
def image(path = nil)
|
5
|
+
@instance ||= begin
|
6
|
+
file = File.open asset_path(path || 'muffin-milk.jpg')
|
7
|
+
Useless::Museum::Image.new(file)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def assert_image_resize(image, length)
|
12
|
+
if image[:width] > image[:height]
|
13
|
+
image[:width].should == length
|
14
|
+
elsif image[:height] > image[:width]
|
15
|
+
image[:height].should == length
|
16
|
+
else
|
17
|
+
image[:width].should == length and image[:height].should == length
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
describe '#small' do
|
22
|
+
it 'should return a MiniMagick::Image object whose longest dimension is 100 pixels' do
|
23
|
+
small_image = image.small
|
24
|
+
small_image.should be_a(MiniMagick::Image)
|
25
|
+
assert_image_resize small_image, 100
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
describe '#medium' do
|
30
|
+
it 'should return a MiniMagick::Image object whose longest dimension is 500 pixels' do
|
31
|
+
medium_image = image.medium
|
32
|
+
medium_image.should be_a(MiniMagick::Image)
|
33
|
+
assert_image_resize medium_image, 500
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
describe '#large' do
|
38
|
+
it 'should return a MiniMagick::Image object whose longest dimension is 1024 pixels' do
|
39
|
+
large_image = image.large
|
40
|
+
large_image.should be_a(MiniMagick::Image)
|
41
|
+
assert_image_resize large_image, 1024
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# These specs aren't particularly useful as you have to go on faith that
|
46
|
+
# muffin-milk.jpg actually has these values embedded - I guess these can act
|
47
|
+
# as a sanity check
|
48
|
+
describe '#latitude' do
|
49
|
+
it 'should return the latitude value exracted from the image' do
|
50
|
+
image.latitude.to_i.should == 40
|
51
|
+
end
|
52
|
+
|
53
|
+
it 'should return nil if the image has no metadata' do
|
54
|
+
image('muffin-milk-stripped.jpg').latitude.should be_nil
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
describe '#longitude' do
|
59
|
+
it 'should return the latitude value exracted from the image' do
|
60
|
+
image.longitude.to_i.should == -73
|
61
|
+
end
|
62
|
+
|
63
|
+
it 'should return nil if the image has no metadata' do
|
64
|
+
image('muffin-milk-stripped.jpg').longitude.should be_nil
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
describe '#shot_at' do
|
69
|
+
it 'shoulr return the time that the image was taken' do
|
70
|
+
image.shot_at.should == Time.parse('2011-11-14T02:23:42Z')
|
71
|
+
end
|
72
|
+
|
73
|
+
it 'should return nil if the image has no metadata' do
|
74
|
+
image('muffin-milk-stripped.jpg').shot_at.should be_nil
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
describe '#valid?' do
|
79
|
+
it 'should return true if the image is a JPEG' do
|
80
|
+
image('muffin-milk.jpg').should be_valid
|
81
|
+
end
|
82
|
+
|
83
|
+
it 'should return true if the image is a PNG' do
|
84
|
+
image('muffin-milk.png').should be_valid
|
85
|
+
end
|
86
|
+
|
87
|
+
it 'should return true if the image is a TIFF' do
|
88
|
+
image('muffin-milk.tiff').should be_valid
|
89
|
+
end
|
90
|
+
|
91
|
+
it 'should return true if the image is a GIF' do
|
92
|
+
image('muffin-milk.gif').should be_valid
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
data/spec/museum_spec.rb
ADDED
@@ -0,0 +1,150 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
2
|
+
|
3
|
+
require 'useless'
|
4
|
+
|
5
|
+
describe 'museum.useless.io' do
|
6
|
+
include Rack::Test::Methods
|
7
|
+
|
8
|
+
def app
|
9
|
+
Useless::Rack.new do
|
10
|
+
map 'museum' => Useless::Museum
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
describe 'GET /photos/:id' do
|
15
|
+
it 'should return the record corresponding to the specified ID' do
|
16
|
+
shot_at = Time.now - (24 * 60 * 60)
|
17
|
+
created_at = Time.now
|
18
|
+
user_id = Useless.mongo['users'] << {handle: 'khy'}
|
19
|
+
photo_id = Useless.mongo['museum.photos'] << {
|
20
|
+
file_url: {
|
21
|
+
small: 'http://museum.useless.io/files/abc123',
|
22
|
+
medium: 'http://museum.useless.io/files/def456',
|
23
|
+
large: 'http://museum.useless.io/files/ghi789'
|
24
|
+
},
|
25
|
+
latitude: 71.567,
|
26
|
+
longitude: 48.987,
|
27
|
+
shot_at: shot_at,
|
28
|
+
description: 'An image of the Scientology headquarters',
|
29
|
+
created_by_id: user_id,
|
30
|
+
created_at: created_at,
|
31
|
+
updated_at: created_at
|
32
|
+
}
|
33
|
+
|
34
|
+
get "http://museum.useless.io/photos/#{photo_id}"
|
35
|
+
last_response.should be_ok
|
36
|
+
|
37
|
+
body = JSON.parse(last_response.body)
|
38
|
+
body['id'].should == photo_id.to_s
|
39
|
+
body['file_url']['small'].should == 'http://museum.useless.io/files/abc123'
|
40
|
+
body['file_url']['medium'].should == 'http://museum.useless.io/files/def456'
|
41
|
+
body['file_url']['large'].should == 'http://museum.useless.io/files/ghi789'
|
42
|
+
body['latitude'].should == 71.567
|
43
|
+
body['longitude'].should == 48.987
|
44
|
+
body['shot_at'].should == shot_at.utc.iso8601
|
45
|
+
body['description'].should == 'An image of the Scientology headquarters'
|
46
|
+
body['created_by']['id'].should == user_id.to_s
|
47
|
+
body['created_by']['handle'].should == 'khy'
|
48
|
+
body['created_at'].should == created_at.utc.iso8601
|
49
|
+
body['updated_at'].should == created_at.utc.iso8601
|
50
|
+
end
|
51
|
+
|
52
|
+
it 'should return a 404 Not Found if there is no record corresponding to the specified ID' do
|
53
|
+
get "http://museum.useless.io/photos/#{BSON::ObjectId.new.to_s}"
|
54
|
+
last_response.should be_not_found
|
55
|
+
end
|
56
|
+
|
57
|
+
it 'should return a 404 Not Found if the specified ID is not a valid BSON::ObjectId' do
|
58
|
+
get "http://museum.useless.io/photos/invalid-bson-object-id"
|
59
|
+
last_response.should be_not_found
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
describe 'POST /photos' do
|
64
|
+
it 'should create a new photo record if latitude, longitude and shot_at are not specified,
|
65
|
+
but can be extracted from the file and the request is authenticated' do
|
66
|
+
user_id = Useless.mongo['users'] << {handle: 'khy', access_token: 'abc123'}
|
67
|
+
|
68
|
+
header 'Authorization', 'abc123'
|
69
|
+
post 'http://museum.useless.io/photos',
|
70
|
+
file: upload_file('muffin-milk.jpg', 'image/jpeg'),
|
71
|
+
description: 'The Muffin Milk Man!'
|
72
|
+
|
73
|
+
last_response.status.should == 201
|
74
|
+
get last_response.header['Location']
|
75
|
+
last_response.should be_ok
|
76
|
+
|
77
|
+
body = JSON.parse(last_response.body)
|
78
|
+
body['latitude'].to_i.should == 40
|
79
|
+
body['longitude'].to_i.should == -73
|
80
|
+
body['shot_at'].should == '2011-11-14T02:23:42Z'
|
81
|
+
body['description'].should == 'The Muffin Milk Man!'
|
82
|
+
body['created_by']['id'].should == user_id.to_s
|
83
|
+
body['created_by']['handle'].should == 'khy'
|
84
|
+
Time.parse(body['created_at']).should be_within(2).of(Time.now)
|
85
|
+
Time.parse(body['updated_at']).should be_within(2).of(Time.now)
|
86
|
+
|
87
|
+
get body['file_url']['small']
|
88
|
+
image = MiniMagick::Image.read(last_response.body)
|
89
|
+
[image[:width], image[:height]].should include(100)
|
90
|
+
|
91
|
+
get body['file_url']['medium']
|
92
|
+
image = MiniMagick::Image.read(last_response.body)
|
93
|
+
[image[:width], image[:height]].should include(500)
|
94
|
+
|
95
|
+
get body['file_url']['large']
|
96
|
+
image = MiniMagick::Image.read(last_response.body)
|
97
|
+
[image[:width], image[:height]].should include(1024)
|
98
|
+
end
|
99
|
+
|
100
|
+
it 'should respond with a 401 Unauthorized if no access code is specified' do
|
101
|
+
post 'http://museum.useless.io/photos',
|
102
|
+
file: upload_file('muffin-milk.jpg', 'image/jpeg'),
|
103
|
+
description: 'The Muffin Milk Man!'
|
104
|
+
last_response.status.should == 401
|
105
|
+
last_response.body.should == 'The request could not be authenticated.'
|
106
|
+
end
|
107
|
+
|
108
|
+
it 'should respond with a 422 Unprocessible Entry if the latitude, longitude and shot at are
|
109
|
+
not specified and cannot be extracted' do
|
110
|
+
user_id = Useless.mongo['users'] << {handle: 'khy', access_token: 'abc123'}
|
111
|
+
|
112
|
+
header 'Authorization', 'abc123'
|
113
|
+
post 'http://museum.useless.io/photos',
|
114
|
+
file: upload_file('muffin-milk-stripped.jpg', 'image/jpeg'),
|
115
|
+
description: 'The Muffin Milk Man!'
|
116
|
+
last_response.status.should == 422
|
117
|
+
last_response.body.should == 'latitude, longitude or shot_at could not be extracted from the photo. Retry with the latitude, longitude and shot_at attributes specified.'
|
118
|
+
end
|
119
|
+
|
120
|
+
it 'should respond with 201 if latitude, longitude and shot_at cannot be extracted but are specified' do
|
121
|
+
user_id = Useless.mongo['users'] << {handle: 'khy', access_token: 'abc123'}
|
122
|
+
|
123
|
+
header 'Authorization', 'abc123'
|
124
|
+
post 'http://museum.useless.io/photos',
|
125
|
+
file: upload_file('muffin-milk-stripped.jpg', 'image/jpeg'),
|
126
|
+
description: 'The Muffin Milk Man!',
|
127
|
+
latitude: 71.567,
|
128
|
+
longitude: 48.987,
|
129
|
+
shot_at: (Time.now - (24 * 60 * 60)).utc.iso8601
|
130
|
+
|
131
|
+
last_response.status.should == 201
|
132
|
+
get last_response.header['Location']
|
133
|
+
last_response.should be_ok
|
134
|
+
end
|
135
|
+
|
136
|
+
it 'should respond with a 422 if the file type is invalid, even if latitude, longitude and shot at are specified' do
|
137
|
+
user_id = Useless.mongo['users'] << {handle: 'khy', access_token: 'abc123'}
|
138
|
+
|
139
|
+
header 'Authorization', 'abc123'
|
140
|
+
post 'http://museum.useless.io/photos',
|
141
|
+
file: upload_file('muffin-milk.txt', 'text/plain'),
|
142
|
+
latitude: 71.567,
|
143
|
+
longitude: 48.987,
|
144
|
+
shot_at: (Time.now - (24 * 60 * 60)).utc.iso8601
|
145
|
+
|
146
|
+
last_response.status.should == 422
|
147
|
+
last_response.body.should == 'Invalid image format. Format must be one of JPEG, TIFF, PNG or GIF.'
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
ENV['RACK_ENV'] = 'test'
|
2
|
+
require File.dirname(__FILE__) + '/../lib/useless/museum'
|
3
|
+
|
4
|
+
require 'rubygems'
|
5
|
+
require 'bundler/setup'
|
6
|
+
|
7
|
+
require 'rack/test'
|
8
|
+
require 'useless/rack'
|
9
|
+
|
10
|
+
RSpec.configure do |config|
|
11
|
+
config.order = :rand
|
12
|
+
|
13
|
+
def clean_database
|
14
|
+
Useless.mongo.db.collections.each do |collection|
|
15
|
+
if !(collection.name =~ /^system\./)
|
16
|
+
collection.drop
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
config.before(:suite){ clean_database }
|
22
|
+
|
23
|
+
config.after(:each) do
|
24
|
+
clean_database
|
25
|
+
|
26
|
+
# After dropping collections, we need to force a new connection on the
|
27
|
+
# next request.
|
28
|
+
Useless.mongo.reset_connection!
|
29
|
+
end
|
30
|
+
|
31
|
+
def upload_file(path, content_type = nil)
|
32
|
+
Rack::Test::UploadedFile.new(asset_path(path), content_type)
|
33
|
+
end
|
34
|
+
|
35
|
+
def asset_path(path)
|
36
|
+
File.dirname(__FILE__) + '/support/assets/' + path
|
37
|
+
end
|
38
|
+
end
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
@@ -0,0 +1 @@
|
|
1
|
+
MUFFIN MILK!
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$LOAD_PATH.unshift(File.expand_path('../lib', __FILE__))
|
3
|
+
require 'useless/museum'
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = 'useless-museum'
|
7
|
+
spec.version = Useless::Museum::VERSION
|
8
|
+
spec.authors = ['Kevin Hyland']
|
9
|
+
spec.email = ['khy@me.com']
|
10
|
+
spec.summary = 'The Museum of Whatever'
|
11
|
+
spec.description = 'The Museum of Whatever'
|
12
|
+
spec.homepage = 'http://museum.useless.io'
|
13
|
+
spec.license = 'MIT'
|
14
|
+
|
15
|
+
spec.files = `git ls-files`.split("\n")
|
16
|
+
spec.test_files = spec.files.grep(%r{^spec/})
|
17
|
+
spec.require_paths = ['lib']
|
18
|
+
|
19
|
+
spec.add_runtime_dependency 'sinatra', '~> 1.4.2'
|
20
|
+
spec.add_runtime_dependency 'mini_magick', '~> 3.5.0'
|
21
|
+
spec.add_runtime_dependency 'exifr', '~> 1.1.3'
|
22
|
+
spec.add_runtime_dependency 'useless', '~> 0.2.0'
|
23
|
+
spec.add_runtime_dependency 'useless-doc', '~> 0.6.1'
|
24
|
+
|
25
|
+
spec.add_development_dependency 'rspec', '~> 2.13.0'
|
26
|
+
spec.add_development_dependency 'rack-test', '~> 0.6.2'
|
27
|
+
spec.add_development_dependency 'oj', '~> 2.0.10'
|
28
|
+
end
|
metadata
ADDED
@@ -0,0 +1,185 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: useless-museum
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.2.3
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Kevin Hyland
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2013-03-29 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: sinatra
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ~>
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 1.4.2
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ~>
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 1.4.2
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: mini_magick
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ~>
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 3.5.0
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ~>
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 3.5.0
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: exifr
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ~>
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 1.1.3
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ~>
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 1.1.3
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: useless
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ~>
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 0.2.0
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ~>
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 0.2.0
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: useless-doc
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ~>
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 0.6.1
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ~>
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 0.6.1
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rspec
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ~>
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: 2.13.0
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ~>
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: 2.13.0
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: rack-test
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ~>
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: 0.6.2
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ~>
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: 0.6.2
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: oj
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ~>
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: 2.0.10
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ~>
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: 2.0.10
|
125
|
+
description: The Museum of Whatever
|
126
|
+
email:
|
127
|
+
- khy@me.com
|
128
|
+
executables: []
|
129
|
+
extensions: []
|
130
|
+
extra_rdoc_files: []
|
131
|
+
files:
|
132
|
+
- .gitignore
|
133
|
+
- .rbenv-version
|
134
|
+
- Gemfile
|
135
|
+
- RESEARCH.md
|
136
|
+
- Rakefile
|
137
|
+
- lib/useless/museum.rb
|
138
|
+
- lib/useless/museum/image.rb
|
139
|
+
- lib/useless/museum/version.rb
|
140
|
+
- spec/documentation_spec.rb
|
141
|
+
- spec/museum/image_spec.rb
|
142
|
+
- spec/museum_spec.rb
|
143
|
+
- spec/spec_helper.rb
|
144
|
+
- spec/support/assets/muffin-milk-stripped.jpg
|
145
|
+
- spec/support/assets/muffin-milk.gif
|
146
|
+
- spec/support/assets/muffin-milk.jpg
|
147
|
+
- spec/support/assets/muffin-milk.png
|
148
|
+
- spec/support/assets/muffin-milk.tiff
|
149
|
+
- spec/support/assets/muffin-milk.txt
|
150
|
+
- useless-museum.gemspec
|
151
|
+
homepage: http://museum.useless.io
|
152
|
+
licenses:
|
153
|
+
- MIT
|
154
|
+
metadata: {}
|
155
|
+
post_install_message:
|
156
|
+
rdoc_options: []
|
157
|
+
require_paths:
|
158
|
+
- lib
|
159
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
160
|
+
requirements:
|
161
|
+
- - '>='
|
162
|
+
- !ruby/object:Gem::Version
|
163
|
+
version: '0'
|
164
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
165
|
+
requirements:
|
166
|
+
- - '>='
|
167
|
+
- !ruby/object:Gem::Version
|
168
|
+
version: '0'
|
169
|
+
requirements: []
|
170
|
+
rubyforge_project:
|
171
|
+
rubygems_version: 2.0.0
|
172
|
+
signing_key:
|
173
|
+
specification_version: 4
|
174
|
+
summary: The Museum of Whatever
|
175
|
+
test_files:
|
176
|
+
- spec/documentation_spec.rb
|
177
|
+
- spec/museum/image_spec.rb
|
178
|
+
- spec/museum_spec.rb
|
179
|
+
- spec/spec_helper.rb
|
180
|
+
- spec/support/assets/muffin-milk-stripped.jpg
|
181
|
+
- spec/support/assets/muffin-milk.gif
|
182
|
+
- spec/support/assets/muffin-milk.jpg
|
183
|
+
- spec/support/assets/muffin-milk.png
|
184
|
+
- spec/support/assets/muffin-milk.tiff
|
185
|
+
- spec/support/assets/muffin-milk.txt
|