useless-museum 1.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|