Latest from the Bamboo Blog

8

Sexy Forms for Merb matt

July 4th, 2008

I've written a little lib which piggy backs on the standard merb form helpers to give you nicer looking forms with help from Andy and Martyn.

We weren't happy with our forms and we were writing too much code. So we wrapped some goodies around the merb form helpers. We've got notes, inline errors, required / not required formatting, and cancel buttons. The API is so minimal you won't be able to resist.

Here's an example of the ruby code, the generated HTML, and what it looks like with some sexy CSS:

Example Code

Ruby

<%= field(:text, :nickname, :required => true, :note => "You'll be known by this on the site. Don't use your real name. It has to be unique, and you can't change it later.")%>
<%= field(:password, :password, :required => true)%>
<%= field(:password, :password_confirmation, :required => true, :note => "Type the same password again, just to make sure we've got it right" )%>
<%= field(:checkbox, :terms_and_conditions, :required => true, :value => true, :label=>"I accept the terms and conditions")%>
<%= form_submit "Sign Up!" %>

Html Generated

<div class="field password required">
<label for="user_password">Password</label>
<input type="password" class="password" name="user[password]" id="user_password" />
</div>

<div class="field password required">
<label for="user_password_confirmation">Password confirmation</label>
<input type="password" class="password" name="user[password_confirmation]" id=
"user_password_confirmation" />

<p class="note">Type the same password again, just to make sure we've got it right
<abbr title="smile">: )</abbr></p>
</div>

<div class="field checkbox required">
<input type="hidden" class="hidden" name="user[terms_and_conditions]" value="0" />
<input type="checkbox" class="checkbox" name="user[terms_and_conditions]" value="1"
id="user_terms_and_conditions" />
<label for="user_terms_and_conditions">I accept the terms and conditions</label>
</div>

<div class="field controls">
<button type="submit" class="positive">Sign Up!</button>
</div>

Html with Errors

<div class="field error password required">
<label for="user_password">Password</label>
<input type="password" class="error password" name="user[password]" id="user_password" />

<p class="error">can't be blank and is too short (minimum is 4 characters)</p>
</div>

<div class="field error password required">
<label for="user_password_confirmation">Password confirmation</label>
<input type="password" class="error password" name="user[password_confirmation]" id="user_password_confirmation" />

<p class="note">Type the same password again, just to make sure we've got it right
<abbr title="smile">: )</abbr></p>

<p class="error">can't be blank</p>
</div>

<div class="field error checkbox required">
<input type="hidden" class="hidden" name="user[terms_and_conditions]" value="0" />
<input type="checkbox" class="error checkbox" name="user[terms_and_conditions]" value="1" id="user_terms_and_conditions" />
<label for="user_terms_and_conditions">I accept the terms and conditions</label>

<p class="error">must be accepted</p>
</div>

Sexy Form

sexyform

You can be the judge of the sexyness of these forms, but they are much nicer than the default error message in my opinion.

Code/CSS

Code is licensed under the MIT License, and under no guarantee that it will not break if they change the external API for form helpers.

Install

Add the file in your lib folder, add the dependency in your init.rb and then include in your global helpers.

Enjoy!

4

APICache martyn

June 6th, 2008

You want to use the Twitter API but you don't want to die? I have the solution to API caching:

APICache.get("http://twitter.com/statuses/public_timeline.rss")

You get the following functionality for free:

  • New data every 10 minutes
  • If the twitter API dies then keep using the last data received for a day. Then assume it's invalid and announce that Twitter has FAILED (optional).
  • Don't hit the rate limit (70 requests per 60 minutes)

So what exactly does APICache do? Given cached data less than 10 minutes old, it returns that. Otherwise, assuming it didn't try to request the URL within the last minute (to avoid the rate limit), it makes a get request to the Twitter API. If the Twitter API timeouts or doesn't return a 2xx code (very likely) we're still fine: it just returns the last data fetched (as long as it's less than a day old). In the exceptional case that all is lost and no data can be returned, it raises an APICache::NotAvailableError exception. You're responsible for catching this exception and complaining bitterly to the internet.

All very simple. What if you need to do something more complicated? Say you need authentication or the silly API you're using doesn't follow a nice convention of returning 2xx for success. Then you need a block:

APICache.get('twitter_replies', :cache => 3600) do
  Net::HTTP.start('twitter.com') do |http|
    req = Net::HTTP::Get.new('/statuses/replies.xml')
    req.basic_auth 'username', 'password'
    response = http.request(req)
    case response
    when Net::HTTPSuccess
      # 2xx response code
      response.body
    else
      raise APICache::Invalid
    end
  end
end

All the caching is still handled for you. If you supply a block then the first argument to APICache.get is assumed to be a unique key rather than a URL. Throwing APICache::Invalid signals to APICache that the request was not successful.

You can send any of the following options to APICache.get(url, options = {}, &block). These are the default values (times are all in seconds):

{
  :cache => 600,    # 10 minutes  After this time fetch new data
  :valid => 86400,  # 1 day       Maximum time to use old data
                    #             :forever is a valid option
  :period => 60,    # 1 minute    Maximum frequency to call API
  :timeout => 5     # 5 seconds   API response timeout
}

Before using the APICache you need to initialize the caches. In merb, for example, put this in your init.rb:

APICache.start

Currently there are two stores available: MemcacheStore and MemoryStore. MemcacheStore is the default but if you'd like to use MemoryStore, or another store - see AbstractStore, just supply it to the start method:

APICache.start(APICache::MemoryStore)

I suppose you'll want to get your hands on this magic. For now just grab the source from http://github.com/mloughran/api_cache/tree/master and rake install. I'll get a gem sorted soon.

This is the irb quickstart so that you don't have to re-parse the above:

require 'rubygems'
require 'api_cache'
APICache.start(APICache::MemoryStore)
APICache.get("http://twitter.com/statuses/public_timeline.rss")

Please send feedback if you think of any other functionality that would be handy.

2

Integration testing Merb with Webrat gwyn

April 23rd, 2008

RSpec stories are a way of doing integration and acceptance testing using plaintext executable tests. You can use them in Merb as well as Rails. Here's how.

Setting up

Install edge Merb; the latest gem (0.9.2) will not work. You need merb-core, merb-more, and merb-plugins.

Merb-plugins gives you the merb_stories gem, so you don't need to install that separately.

Add this line to your app's config/environments/test.rb:

dependencies "merb_stories", "webrat"

(Note that merb_stories' README file is wrong about this, for now - and the generator will create a dependency on merb-rspec, which no longer exists. My fork fixes this.)

Now generate your story:

merb-gen story mystory

Now run your story:

rake story\[mystory\]

Yes, you must include the square brackets, and you have to escape them.

Writing stories

Now fill out your story. There are some differences to Rails' versions. The best places to look for help are in the Merb code itself:

  • spec/public/test/controller _matchers _spec.rb
  • lib/merb-core/test/helpers
  • lib/merb-core/test/matchers

To start you off, here are the steps for a simple integration test:

steps_for(:homepage) do
  When("I visit the root") do
    @mycontroller = get("/")
  end
  Then("I should see the home page") do
    @mycontroller.should respond_successfully
    @mycontroller.body.should contain("Hello") 
  end    
end

As you write your tests, don't trust Merb absolutely. Some things are wrong, don't work, or aren't meant to work [yet]. As part of debugging, look at the Merb source, and fork it and fix it if needed.

Adding Webrat

Webrat lets you write integration tests that are even closer to natural language. You can say things like:

visits '/auth/login'
fills_in 'username',:with=>"bob"
fills_in 'password',:with=>"hunter2"
clicks_button "login"
response.should be_successful
...

I've forked Webrat to add Merb support.

To get started, clone it, build the gem and install it.

You'll also need to make sure you're using the memory session store (for testing), or your sessions won't be preserved:

Merb::Config.use do |c|
  c[:session_store] = 'memory' 
end

Continuous integration

Autotest will run your specs continuously, but won't run your stories. You can fix this, but stories run very slowly, since they use the full stack. At the very least, though, you want to run your stories after you deploy, in case you have "works on dev machine, dies on production" problems.

Add this to deploy.rb:

namespace :deploy do
  task :after_deploy do
    run_remote_tests
  end
  desc "Run tests on remote server"
  task :run_remote_tests do
    run "cd #{deploy_to}/current && rake spec"
    run "cd #{deploy_to}/current && rake story[all] MERB_ENV=test"
  end 
end

This will fail if you're using Vendor Everything like us - it won't be able to find merb-core. To fix it, replace stories/stories/all.rb:

env = ENV['MERB_ENV'] || 'test'
require 'rubygems'
Gem.clear_paths
Gem.path.unshift(File.join(File.dirname(__FILE__), "..","..","gems"))

require 'merb-core'
Merb.load_dependencies(:environment => env)

require 'spec'
Merb.start_environment(:testing => true, :adapter => 'runner', :environment => env)

dir = File.dirname(__FILE__)
Dir[File.expand_path("#{dir}/**/*.rb")].uniq.each do |file|
  require file
end

You'll have the same problem with specs. Edit spec/spec_helper.rb:

Gem.clear_paths
Gem.path.unshift(File.join(File.dirname(__FILE__), "..","gems"))

Now you have natural-language integration tests that run automatically on deployment. Sweet.

10

Merb Book (Part 3) matt

April 18th, 2008

Here we are going cover the install instructions for Merb, RSpec, and DataMapper (0.9) and how to create a bare application. The next post will cover aspects of the framework and introduce the example application we will be building.

Read the rest of this entry
1

Merb and DataMapper Book (Part 2) matt

April 17th, 2008

Here is the next part of the Merb, DataMapper, RSpec book. I was planning on releasing more today but I am rewriting the examples for DataMapper 0.9. There is a example app for DataMapper 0.3 in the git repository of the book, if you can't wait to play with it.

Read the rest of this entry
5

Merb and DataMapper Book (Part 1) matt

April 9th, 2008

When I started learning Merb and DataMapper I kept a collection of notes to help me keep up with these projects. These grew to the point that they couldn't fit nicely into a single text file. With contributions from others I started to put together a small book on developing web apps with Merb and DataMapper. I have decided to release it as we go. There is also a project in GitHub if you want to check it out and contribute.

So here we go (corrections and comments welcome):

Life On The Edge With Merb, DataMapper & RSpec

Read the rest of this entry
1

Euruko 2008 matt

March 30th, 2008

We're at Euroko in Prague, with some of the guys at New Bamboo. It's been pretty good so far (crippling flu aside) and Prague is a lovely city.

I gave a talk today on Aspect Orientated Programming in Ruby using Aquarium, it quickly covered what I mentioned in my last post on AOP, and went into more detail with actual code examples this time!

The slides are on slideshare, the demo code is currently in a git repo . The demo app is Merb/Datamapper and I'm currently trying think of more examples of cool aspects to put in there, feel free to add some.

If anyone saw my talk, I'd love to hear what you thought of it so drop me a line matt@new-bamboo.co.uk or leave a comment on this post.

2

QCON London 2008 matt

March 16th, 2008

I’ve just spent the last week at QCon and I’ve just about fully recovered (it was pretty intense). Considering there was only one Ruby track I was a little worried that I wouldn’t find most of the talks interesting, however this wasn’t the case. The most interesting talks had very little to do with Ruby.

When I go to conferences, my main goal isn’t to learn lots of things it’s to be inspired and generate new ideas, consequently my “things to do in my spare time” list has grown exponentially.

The two underling themes that I detected from the conference was developers are people too and scaling is important. It was interesting to hear Java guys talk about how to make developing easier (more fun), because this is one area Ruby (in my opinion) excels over most other languages.

One of the main criticisms about the Ruby community is we don’t care about scaling, and scaling is something that the Java guys have been working on for years. So when Ola Bini says that JRuby will be the best stack/platform around, I’m starting to believe him.

Imagine having your Ruby app with objects persisting in something like Terracotta?

Or being able to use existing Java aspects on Ruby code (and visa versa).

There was talk about GemStone porting their Smalltalk VM to Ruby so we may have an ‘enterprise’ stack.

And where the hell are the Ruby Specs? I mean the tests for the language itself. There should be one central repository where YARV, Rubinius, JRuby, GemStone and every Ruby developer out there can contribute to Spec’in out the language.

They are just a handful of ideas that popped into my head. I had a great time at the conference and met many interesting people, hopefully I’ll be able to do it again next year!

The slides (videos soon) from the talks are going online so take a look. Please excuse me while I go play with Erlang.

UPDATE I’ve put some of the slides that are available on the QCon site onto slideshare tagged with QCONLONDON2008 , enjoy.

0

Nested layouts in Merb martyn

March 15th, 2008

First a simple solution using throw_content and catch_content.

This is what sublayout.html.erb looks like

<% throw_content :sublayout do %>
  <div class="header">
    ...
  </div>

  <%= catch_content :for_layout %>

  <div class="footer">
    ...
  </div>
<% end %>

<%= render catch_content(:sublayout), :layout => "application" %>

Your main layout contains <%= catch_content :for_layout %> as usual.


You might find it handy to use catch and throw content in a few more places. For example, in your sublayout you could <%= catch_content :sublayout_title %> and throw it in the view with

<% throw_content :sublayout_title %>
  Hippos
<% end %>

As a little bonus I wrote a helper so that you can use the same syntax as you're used to from the rails plugin. Just add this snippet into Merb::GlobalHelpers

def inside_layout(layout, &block)
  content = render(capture(&block), :layout => layout)
  concat(content, block.binding)
end

Then your sublayout looks like

<% inside_layout :sublayout do %>
  <div class="header">
    ...
  </div>

  <%= catch_content :for_layout %>

  <div class="footer">
    ...
  </div>
<% end %>

Here's another idea which I think would be even better: What if you could say layout ['application', 'sublayout'] in your controller and have each layout rendered inside it's parent all up the chain? Then sublayout.html.erb wouldn't be constrained to always render inside application.html.erb and you could use it in a much more flexible way. However Damien things it's a mad idea. What do you think?

1

Freeze gems in a Merb application martyn

March 14th, 2008

Frozen gems in Merb == gems installed inside the application.

That's clever because now you can use the standard tools for installing and managing the installed gems. For example, you get dependency checking and so on for free. Add -i gems to the command line options to specify the install location.

gem install merb-core -i gems

That's the simplest case (run it from the root of you merb app). It fetches the merb-core gem from the gem servers and installs it inside a gems directory.

However, in many cases that's not what you want. Really you want to freeze the version you're currently developing on. Maybe one you've installed from the merb-core git repository. That's really easy because you have the gem package files on your system already. For me they're in /Library/Ruby/Gems/1.8/cache. Get your location by running gem environment gemdir and look for a cache folder. Install the package with

gem install /Library/Ruby/Gems/1.8/cache/merb-core-0.9.1.gem -i gems

You should add /gems/doc to your .gitignore since you definitely don't want ri and rdoc files in your repository. Alternatively add --no-ri --no-rdoc to the gem install command.

The final challenge is actually getting merb started. Right now I'm relying on a pastie that google found for me (I'm sorry I have no idea who the author is). Make that executable and use it to run merb with the usual merb command line options. That's surely a bit of a hack though and I'm hoping that someone will point out a better way :) Update: Thanks Steve - I hadn't spotted the generator. Run merb-gen freezer frozen-merb and then run Merb with ./script/frozen-merb passing the usual merb command line options.

0

How to use git / github with capistrano martyn

March 12th, 2008

I'm assuming that you're using private key authentication for git and that you've got an ssh agent set up (if you're running Leopard then you automatically have this).

The trick is to use ssh agent forwarding, now supported in capistrano. It allows the server to pull the latest code from github using your local private key and ssh agent. To understand how this trickery works take a look at this illustrated guide.

set :ssh_options, { :forward_agent => true }

One of the great things about git is that it's really fast, even if you're cloning an entire repository. However you can go even faster by using the repository_cache option in capistrano. This essentially keeps a clone of your app on the server and then just does a git pull to fetch new changes and copies the directory across when you deploy.

set :repository_cache, "git_cache"
set :deploy_via, :remote_cache

Another recommended strategy is to specify a git branch. Otherwise you'll default to deploying HEAD which might be some crazy experiment.

set :branch, "stable"

In case you haven't used remote branches they're really easy. Say you have a local stable branch. Push this to github using

git push github stable

So the complete capistrano file should look something like

set :application, "your cool app"

# Source code
set :scm, :git
set :repository, "git@github.com:githubusername/gitrepo.git"
set :branch, "stable"
set :repository_cache, "git_cache"
set :deploy_via, :remote_cache
set :ssh_options, { :forward_agent => true }

# Deployment servers
role :app, "your server"
role :web, "your server"
role :db,  "your server", :primary => true
set :deploy_to, "/var/www/#{application}"
3

Github + Textmate Wiki bundle = Giki damien

March 11th, 2008

In the past year we've probably gone through about 3 different wikis. Either they don't have things like email notifications with a diff, have their own special markup language or cost what is unwieldy amount for just a wiki.

We've scrapped all of these and have now migrated to a private Github repo containing markdown files managed with the help of the Textmate Wiki Bundle. The simplest solutions are the best.

0

When using the nginx_uploadprogress_module you need to set the X-Progress-ID header in your progress polls. Safari sanitises header names, so X-Progress-ID ends up becoming X-Progress-Id in your request. Unfortunately Firefox does just the oppose renaming X-Progress-Id to X-Progress-ID (but only for this particular header). The current release of nginx_uploadprogress_module is case sensitive, so I've patched it to be case insensitive.

Nginx Upload Progress Module Safari patch

0

HTTP/AJAX debugging with Charles damien

February 29th, 2008

If you ever need to inspect an AJAX request from a browser such as Safari (which doesn't quite have a Firebug equivalent], Charles is a useful debugging proxy.

3

IrregularScience martyn

February 19th, 2008

ImageScience is great. Just look at the code! One beautiful class wraps up FreeImage with some jucy inline C. If you can install FreeImage then nothing can go wrong. Much better than a bazillion lines of RMagick (which sometimes still can't so what you need)!

Simplicity is great but unfortunately ImageScience has this infatuation with squares. I'll explain.

Image science has two low level methods which allow arbitrary cropping and resizing. They look like this:

image.with_crop(left, top, right, bottom)

image.resize(width, height)

However what you really want to do is usually a combination of cropping and resizing. ImageScience helpfully provides:

# Resizes to fit within a square
image.thumbnail(size)

# Resizes and crops to be exactly a square
image.cropped_thumbnail(size)

This is great until you need an avatar that's a fixed ... rectangle. Or an image that will fit exactly inside a ... rectangle.

In this case you need IrregularScience!

You'll get rectangular versions of thumbnail and cropped_thumbnail:

# Resizes to fit within a rectangle
image.resize_within(width, height)

# Resizes and crops to be exactly a rectangle
image.resize_exact(width, height)

I gave my methods slightly different names because I'm like that ;)

There's also the case where you want a exact height or an exact width regardless of the other dimension (ever seen facebook?). That turns out to be a really horrible hack with RMagick involving all sorts of squashing and stretching unless you go really low level. Just follow these incantations and know that no horrible hacks are going on:

image.resize_to_width(width)

image.resize_to_height(height)

To get started download irregular_science.rb and require it. Then adapt this example:

ImageScience.with_image(upload_path) do |image|
  self.thumbnails.each do |name, size|
    image.resize_exact(*size) do |img|
      img.save(attachment_path(name))
    end
  end
end

Enjoy!

P.S. This seems to be my first blog post here - so hi!