Moving Pictures with Sinatra, Part I
Working With Files and Directories.
Today, I’d like to make a website that displays pictures from a directory. I think Sinatra would work well for this. We’ll set that up.
Picture Viewer
Setting up the environment.
I like RVM because it keeps gems in their own hemisphere and doesn’t cause permanent waves with other projects. Set up a gemset called movingPictures, use ruby 2.0, and then make sure we are using that gemset. You can do all that in one simple line.
$ rvm gemset use 2.0.0@movingPictures --create
Now that the playground is ready, why don’t we use TDD for this, too. Make a directory for the app, a test directory, and some blank files to get us started.
$ mkdir movingPictures
$ mkdir movingPictures/test
$ cd movingPictures
$ touch Gemfile
$ touch main.rb
$ touch test/test.rb
Open the Gemfile and add the gems we will need.
Gemfile
source 'http://rubygems.org'
gem 'sinatra'
gem 'rack-test'
Install the gems
$ gem install bundler
$ bundle install
We are now ready to go. Write a test to make sure the app starts.
test/test.rb
require './main'
require 'minitest/autorun'
require 'rack/test'
ENV['RACK_ENV'] = 'test'
class MyTest < MiniTest::Unit::TestCase
include Rack::Test::Methods
def app
Sinatra::Application
end
def test_for_echo
get '/'
assert last_response.ok?
assert_equal "Echo", last_response.body
end
end
We are including the main.rb file, which runs our application, along with the files for testing.
main.rb
require 'sinatra'
get '/' do
"Echo"
end
Go ahead an run the test.
$ ruby test/test.rb
Cool, it works.
Obviously, the app needs to show some pictures. How would you write a test for that?
test/test.rb
…
def test_for_pictures
pictures = load_pictures
assert pictures.length > 0
end
…
See what we did here? Load the pictures and make sure the number of pictures is greater that zero.
Let’s try that. Run the test
$ ruby test/test.rb
Run options: --seed 30840
# Running tests:
.E
Finished tests in 0.041759s, 47.8939 tests/s, 47.8939 assertions/s.
1) Error:
test_for_pictures(MyTest):
NameError: undefined local variable or method `load_pictures' for #<MyTest:0x97e398>
test/test.rb:21:in `test_for_pictures'
…
2 tests, 2 assertions, 0 failures, 1 errors, 0 skips
That’s good. What do you need to do to get rid of that error? Add the missing method load_pictures
to the main.rb file. Once you’ve done that run the test and see what you get.
main.rb
require 'sinatra'
def load_pictures
end
get '/' do
"Echo"
end
Let’s run the test again.
$ ruby test/test.rb
Run options: --seed 61820
# Running tests:
E.
Finished tests in 0.041278s, 48.4520 tests/s, 48.4520 assertions/s.
1) Error:
test_for_pictures(MyTest):
NoMethodError: undefined method `length' for nil:NilClass
test/test.rb:22:in `test_for_pictures'
…
2 tests, 2 assertions, 0 failures, 1 errors, 0 skips
Awesome. One error solved reveals another. No biggie, you’ve shown you’re good at solving errors. Let’s solve that one.
Is anything being returned from our load_pictures
method? There’s the problem.
Where are the pictures? I guess we hadn’t thought about that. Sometimes, we just shouldn’t jump right into coding or should we? Since we’re using TDD, if we start moving things around we will find out if stuff breaks. Sweet.
You need to store pictures somewhere. How about in a directory off the root of the application. Let’s use slideshowpictures_ as the directory name. Yes, that’s incredibly long, but it is obvious what goes into that directory.
Go ahead and make that directory.
Wait! Maybe this is where we need to think about this a little. We don’t want to rush into anything.
If we make that directory off of the root, then we’ll have to do some routing work. If we make a public directory off of the root directory, and make a slideshowpictures_ directory inside it, that will solve some unnecessary coding. A show of hands for who’s OK with that.
$ mkdir public/
$ mkdir public/slideshow_pictures
Since we are looking for pictures in a directory you need to write the code to do that. If you are not familiar with the Dir class in Ruby check it out.
Let’s use Dir#glob. That one seems all powerful.
main.rb
…
def load_pictures
Dir.glob("public/slideshow_pictures/*")
end
…
Run the tests.
$ ruby test/test.rb
Run options: --seed 25112
# Running tests:
F.
Finished tests in 0.016690s, 119.8322 tests/s, 179.7484 assertions/s.
1) Failure:
test_for_pictures(MyTest) [test/test.rb:22]:
Failed assertion, no message given.
2 tests, 3 assertions, 1 failures, 0 errors, 0 skips
Huh? Failed assertion, no message given. In my code, line 22 is assert pictures.length > 0
The assert method allows you to add a message that will displayed if the assert is false. Let’s roll the bones and add a message to it just to see what happens.
assert pictures.length > 0, "There are no pictures"
Run the test again.
$ ruby test/test.rb
Run options: --seed 735
# Running tests:
F.
Finished tests in 0.016786s, 119.1469 tests/s, 178.7204 assertions/s.
1) Failure:
test_for_pictures(MyTest) [test/test.rb:22]:
There are no pictures
2 tests, 3 assertions, 1 failures, 0 errors, 0 skips
That’s better. Let’s get back to the slideshow. Exit… stage left.
There is nothing in the slideshowpictures_ directory. Go ahead and add a file to that directory and rerun the test.
$ touch public/slideshow_pictures/temp.txt
$ ruby test/test.rb
Run options: --seed 1658
# Running tests:
..
Finished tests in 0.023275s, 85.9291 tests/s, 128.8937 assertions/s.
2 tests, 3 assertions, 0 failures, 0 errors, 0 skips
Yippee! You are now proving there is something in the slideshowpictures_ directory. It’s only a text file, but we want JPGs.
Did you know that Dir#glob allows you to use regular expressions?
Go ahead and change the load_pictures
method to only look for JPGs and rerun the test.
def load_pictures
Dir.glob("public/slideshow_pictures/*.{jpg,JPG}")
end
Did you get the “There are no pictures” error? Add a JPG to the slideshowpictures_ directory and rerun the test. Remember, regular expressions are case sensitive by default.
All tests passing again? Sweet, Now what? I guess display it on the webpage. You know the drill. Write a test.
I have a picture named test.jpg in the slidershowpictures_ directory. When I run my test the image source should be that.
test/test.rb
…
def test_for_echo
get '/'
assert last_response.ok?
assert_equal "slideshow_pictures/test.jpg", last_response.body
end
…
Notice anything with the assert_equal
line? Why not public/slideshowpictures? A file *./public/slideshowpictures/test.jpg* is made available as http://example.com/slideshow_pictures/test.jpg. Here’s a more technical explanation.
Go ahead and run the tests.
$ ruby test/test.rb
Run options: --seed 3602
# Running tests:
F.
Finished tests in 0.020418s, 97.9528 tests/s, 146.9292 assertions/s.
1) Failure:
test_moving_world(MyTest) [test/test.rb:17]:
Expected: "slideshow_pictures/test.jpg"
Actual: "Echo"
2 tests, 3 assertions, 1 failures, 0 errors, 0 skips
Of course, it failed. You need to load the pictures and then write out their “path”
main.rb
…
get '/' do
@pictures = load_pictures
@pictures.each do |picture|
picture
end
end
…
You load pictures into the @pictures
instance variable and then loop though that. Let’s rerun the test.
$ ruby test/test.rb
Run options: --seed 52201
# Running tests:
F.
Finished tests in 0.062974s, 31.7591 tests/s, 47.6387 assertions/s.
1) Failure:
test_moving_world(MyTest) [test/test.rb:17]:
--- expected
+++ actual
@@ -1 +1 @@
-"slideshow_pictures/test.jpg"
+"public/slideshow_pictures/test.jpg"
2 tests, 3 assertions, 1 failures, 0 errors, 0 skips
Ugh. It’s like you need to substitute public/ with ” from the pictures path.
Any ideas on how to do that?
main.rb
…
@pictures.each do |picture|
picture.sub!(/public\//, '')
end
…
You’re not going to need that public/ so we got rid of it. This should never come back to bite us, right? :-)
Run the test again.
$ ruby test/test.rb
Run options: --seed 21438
# Running tests:
..
Finished tests in 0.043231s, 46.2631 tests/s, 69.3946 assertions/s.
2 tests, 3 assertions, 0 failures, 0 errors, 0 skips
Presto! It’s working. Now you can write the code for a browser to display the picture. With Sinatra, we can use Inline Templates to render our output. Inline templates are defined in the main application file itself. They are located at the bottom of the file.
main.rb
require 'sinatra'
def load_pictures
Dir.glob("public/slideshow_pictures/*.{jpg,JPG}")
end
get '/' do
@pictures = load_pictures
erb :index
end
__END__
@@index
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="user-scalable=yes, width=device-width" />
<title>Moving Pictures</title>
</head>
<body>
<% @pictures.each do |picture| %>
<img src="<%= picture.sub!(/public\//, '') %>" />
<% end %>
</body>
</html>
We made a little html page. In the get
method Erb is specified as our template language. Also, note that the name of the view is a symbol. Inline templates require that you create a class variable of the same name so Sinatra knows which template to render. We’ll see this later.
Do you think if we run our tests that they will pass? Go ahead and run it.
$ ruby test/test.rb
Run options: --seed 24993
# Running tests:
.F
Finished tests in 0.027037s, 73.9727 tests/s, 110.9591 assertions/s.
1) Failure:
test_moving_world(MyTest) [test/test.rb:17]:
--- expected
+++ actual
@@ -1 +1,14 @@
-"slideshow_pictures/test.jpg"
+"<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset=\"UTF-8\">
+ <meta name=\"viewport\" content=\"user-scalable=yes, width=device-width\" />
+<title>Moving Pictures</title>
+</head>
+<body>
+
+ <img src=\"slideshow_pictures/test.jpg\" />
+
+</body>
+</html>
+"
2 tests, 3 assertions, 1 failures, 0 errors, 0 skips
Yup, mine blew up too.
Now we’re outputting HTML and not just the picture path. We should rewrite the test to check if the body includes “slideshow_pictures/test.jpg”
test/test.rb
…
def test_for_echo
get '/'
assert last_response.ok?
assert last_response.body.include?("slideshow_pictures/test.jpg")
end
…
Holds Breath Rerun the test.
$ ruby test/test.rb
Run options: --seed 48502
# Running tests:
..
Finished tests in 0.043955s, 45.5011 tests/s, 68.2516 assertions/s.
2 tests, 3 assertions, 0 failures, 0 errors, 0 skips
This should work in the browser. Start Sinatra and check it out in the browser. Now, all the world’s a stage.
Let’s go over what we did.
- We learned to read files from a directory.
- We learned how to look for certain file types.
- We learned about the default folder structure of a Sinatra app.
- We learned to set our own error messages.
What’s next?
You might be asking “This is neat and all but how do I upload pictures into the the slideshowpictures_ directory?” That’s the next part.