Moving Pictures with Sinatra, Part I

This entry is part 1 of 3 in the series Moving Pictures with Sinatra

Moving Pictures with Sinatra

mp_sinatra

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.

mp-a-little-husky

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.

Moving Pictures with Sinatra

Moving Pictures with Sinatra, Part II >>

Free book: Jump Start HTML5 Basics

Grab a free copy of one our latest ebooks! Packed with hints and tips on HTML5's most powerful new features.

  • Rodney Blevins

    Hi, thanks for this article, I can’t wait to go through it. However, I’m getting several errors. When I try to run the test, I get the error:
    /System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/1.8/rubygems/custom_require.rb:31:in `gem_original_require': no such file to load — minitest/autorun (LoadError)
    from /System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/1.8/rubygems/custom_require.rb:31:in `require’
    from test/test.rb:3

    Any thoughts?

    • Rodney Blevins

      UPDATE: After many painful hours/days later, I finally had to uninstall macports and fink and upgrade to ruby 1.9.3, reinstall sinatra gem and it’s working now (so far).

  • smire

    Three Rush references in the first paragraph……nice.

  • Rodney Blevins

    UPDATE: I figured it out. I did “gem install minitest” and now it works. I guess you’re assuming people already have minitest installed, no?