Wednesday, July 2, 2008

Contributing to Rails is easier than you think

Rails_2At OtherInbox we love open source and are looking for ways to share some of our labors with the community. Today I came across a great opportunity to contribute something to Ruby-on-Rails core development. I'm posting it here so everyone can see how easy it is to contribute.

I was building a JSON API to enable some new awesome features we're working on. Following the JSON request specification, I had the client setting its MIME type to "application/jsonrequest". But this was not causing Rails to recognize the request as JSON and thus the request body was not properly parsed. After doing some digging, I realized that Rails only looks for MIME type "application/json".

Fortunately, MIME type processing is implemented really humanely in Rails, so I whipped up a little patch that adds "application/jsonrequest" as a synonym for the JSON MIME type. First I wrote a test to prove that this was a problem. Once I had a failing test, I added the MIME type, and got my test passing. I followed the git patch instructions on lighthouse, then jumped into IRC #rails-contrib to garner support for it.

I happened to see that Rick Olson, the author of the existing JSON parsing code, was in the chat, so I pinged him with the lighthouse ticket. He tested it and applied it, and now our one line of code is a part of Rails!

Hopefully this will save some future JSON implementer a bit of pain.

Cross-posted from the OtherInbox blog

Labels: ,

Monday, June 2, 2008

RailsConf 2008 Recap

I wrote up a few articles on the OtherInbox blog about my experiences at RailsConf 2008:
The best blow-by-blow coverage so far is from Drew Blas.  Of the ones I attended or heard the best feedback about, I most strongly recommend looking at the slides for these: 

Labels: , ,

Wednesday, May 28, 2008

See you at Railsconf 2008

I'll be leading two Birds of a Feather sessions at RailsConf 2008 that I hope everyone will attend (or flock to):
I was also invited to sign copies of the book Advanced Rails Recipes (to which I contributed a couple of recipes) at Powell's books in Portland on May 30th at 12:30 pm.  Hope to see you there!

I'll be there with the full OtherInbox contingent, so if you're looking for an awesome startup to join, come and track us down.

Labels:

Sunday, May 25, 2008

Using Ruby's Autoload Method To Configure Your App Just-in-Time

Reading The Ruby Programming Language was a great experience — like revisiting a country I thought I knew intimately, but with expert tour guides who showed me whole new landscapes. It's also a good primer on what's changing in Ruby 1.9.

One of my favorite discoveries was Ruby's autoload method. Using autoload, you can associate a constant with a filename to be loaded the first time that constant is referenced, like so:
autoload :BCrypt, 'bcrypt'
autoload :Digest, 'digest/sha2'
The first time the interpreter encounters the constant BCrypt, it will load the file 'bcrypt' from Ruby's current load path, which it assumes will contain the definition of that constant. (Note that autoload takes the name of the constant, in symbol form, not the constant itself).

Here's an example of how useful it can be. OtherInbox uses beanstalkd in a few places where we haven't yet migrated to SQS. I was loading the beanstalk client with a Rails initializer, 'config/initializers/beanstalk.rb':
require 'beanstalk-client'
BEANSTALK = Beanstalk::Pool.new(['localhost:11300'])
Making this initial connection on our production server takes five seconds or more each time I restarted the app or dropped into the console. That doesn't sound like much but when you're doing that a few times a day, it starts to add up. So I moved the beanstalk code out of the initializer and into 'lib/etc/load_beanstalk.rb'. I placed all of my autoloads in a single initializer, 'config/initializers/autoload.rb'. For beanstalk, the statement is:
autoload :BEANSTALK, 'etc/load_beanstalk'
Now, the app starts more quickly, and even better, this library doesn't get loaded into memory by parts of the app that don't need it.

Labels: ,

Tuesday, May 6, 2008

Advanced Rails Recipes out now

I contributed two recipes to the newly-released Advanced Rails Recipes which I highly recommend.  It's got 84 very eye-opening solutions to problems faced by a lot of Rails programmers.  

I feel lucky to be working with cool, open technology that's yielded an opportunity to be part of a project like this.

Labels:

Thursday, May 1, 2008

Looking for Rails developers (who isn't?)

For the past seven months I've been building a cool new consumer web app with some folks in Austin, TX, otherinbox.com/, and we're looking for experienced Ruby On Rails developers. We're a small agile team led by Steven Smith, founder of FiveRuns.

The whole site is Rails-based and makes extensive use of Amazon Web Services (EC2, S3, and SQS) and there's a new awesome challenge every day. More info on our jobs page.

Labels: ,

Sunday, March 23, 2008

Great explanation of Rails auto-loading

I thought Mark Bush did a great job of explaining Rails' auto-loading behavior on the Rails-talk mailing list.  I'm posting it here to help others find it, and so I remember where to find it next time I'm struggling with it.  I've known that Rails automatically requires and loads classes on the fly using standard conventions for module names -> file names, but I had a hard time grasping how it worked.  Mark explains it very concisely.

The Rails list has a low signal-to-noise ratio but it's still worth paying attention to for gems like this.

Labels:

Monday, February 18, 2008

ActiveRecord Double Validation Errors in RSpec

I had a strange error occur in one of my rspec model unit tests today, and I wanted to document it here because my solution (which is a bit of a hack) is the opposite of what worked for other people.

I have a bunch of tests that check to make sure I'm validating various properties of a model. All of a sudden, I started having tests fail because the validations seemed to be adding the same error twice.
'User creation should require domain names to be unique' FAILED
expected 1 error on :domain, got 2
This error only occured when my full suite of tests ran. If I ran the unit test by itself (or even if I just ran only the model tests by themselves), it didn't happen.  Unfortunately I didn't notice whatever it was I did to introduce this error, so I couldn't reverse it.

Several other people have encountered this problem, so I know it's because my tests are leaking state in some way.  Somehow I am doing something during my tests that rspec isn't able to clean up.  Usually it's because the tests are doing something weird with extra require or load statements, which causes multiple copies of the class to be loaded.  Removing these statements usually works.  

I had no such statements and so spent a while trying to sort this out.  On a whim, I added a require statement to the top of the failing User model spec, and that fixed it:
require 'user'
I wish I had more time to investigate, because it makes me thing I don't know enough about Rails autoloading behavior, or Ruby's loading behavior. 

If you're running into this same problem, I found these mailing lists threads to be useful:

Labels: , ,

Tuesday, February 12, 2008

Unobtrusive Firefox Plugin Click-to-Install

I've been working on a really cool new project, to be announced soon, where I've built a Rails-based web app that has two interfaces: one for humans to interact with inside of a browser, and a RESTful API for browser plugins to interact with via GETs and POSTs.  We want people to be able to interact with our site while visiting other sites.


I had seen other Firefox plugins that were click-to-install, but I had a hard time figuring out how to make it work for our plugin. Firefox users were having to "Save Link As.." and open the downloaded .xpi file manually.  Very old-fashioned.  So here's a quick note to help future Mozilla or Firefox developers who need to create a click-to-install plugin:


1) It's all done through Javascript, so anyone without Javascript will have to install your plugin the old-fashioned way.  The Mozilla site documents the API call you need to make.


2) I'm a huge proponent of unobtrusive javascript (UJS), which I learned by using Dan Webb's excellent LowPro framework. Thus I wanted to make sure that the click-to-install javascript was offered as a progressive enhancement to the normal HTML links we provided.  That way, everyone could have a link to the plugin file itself for manual installation, but people with Javascript could enjoy click-to-install.


In this part of the site, we weren't using any other Javascript libraries, so it seemed like overkill to include Prototype and LowPro just for this one effect.  So it was a great chance to learn how to roll my own UJS without library support.  I whipped up a quick UJS click-to-install technique following inspiration from this presentation.  Here's what I came up with:

<script defer="defer" type="text/javascript">
//<![CDATA[
function doXPITrigger() {
if (!document.getElementsByTagName) return false;
var links = document.getElementsByTagName("a");
for (var i=0; i < links.length; i++) {
if (links[i].className.match("firefox")) {
links[i].onclick = function() {
xpi={'Awesome New Project Toolbar':'/downloads/awesome_project.xpi'};
InstallTrigger.install(xpi);
return false;
}
}
}
}

window.onload = doXPITrigger
//]]>

3) I've seen other advice recommending you configure your web server to recognize the .xpi mimetype appropriately.  I did this but it didn't make much difference in my case.  Still, it's probably worth doing.  I added this line to our Apache config:


AddType application/x-xpinstall .xpi

Labels: , ,

Wednesday, January 16, 2008

Autotest with verbose flag on

I followed a tip from David Chelimsky's blog and began running autotest with the -v flag for verbosity.  When you first run it, you get a bunch of lines like this:
Dunno! spec/other_inbox_spec_helpers.rb
Dunno! app/views/layouts/main/_footer.erb
Which show all the files for which autotest doesn't have a mapping.  With ZenTest 3.8.0 out, it's easy to add mappings (which tell autotest which tests to run when a matching file changes) and exceptions (which tell autotest which files to ignore).  You can also set up .autotest files for particular projects, or for your whole development machine (~/.autotest).

Here's an example of a per-project .autotest I use, sitting in the Rails root directory.  I've got a custom spec helper and I want to rerun all my tests if this file ever changes:
Autotest.add_hook :initialize do |at|
%w{ domain_regexp perfdata coverage reports }.each { |exception| at.add_exception(exception) }

at.add_mapping(/spec\/app_spec_helper.rb/) do |_, m|
at.files_matching %r%^spec/(controllers|helpers|lib|models|views)/.*\.rb$%
end

end
And here's part of my site-wide .autotest file, mostly cribbed from David's blog, where I'm ignoring other kinds of cruft that pile up in projects.  Also note the mapping for spec/defaults.rb, a file I commonly setup in my specs containing default parameters for different models.
Autotest.add_hook :initialize do |at|
%w{.hg .git .svn stories tmtags Rakefile Capfile README spec/spec.opts spec/rcov.opts vendor/gems autotest svn-commit .DS_Store }.each {|exception|at.add_exception(exception)}

at.add_mapping(/spec\/defaults.rb/) do |f, _|
at.files_matching %r%^spec/(controllers|helpers|lib|models|views)/.*\.rb$%
end

end

Labels: , , ,

Tuesday, January 8, 2008

Presenter classes help with Rails complexity

A few weeks back I gave a talk at a Bmore on Rails meeting all about Presenter classes, which I learned about from Jay Fields.  I can't claim any authorship of this idea, but it's been very helpful to me, so I thought I'd share some of my examples here plus a bit of extra code I wrote to make Presenter integration with ActiveRecord a bit more fun.

Presenters allow you to extract the logic needed for complex views (especially views that require the use of more than one model) into a separate, easily testable class.  This helps you write clean code and skinny controllers, among other benefits. 

1) The key background material is here:


2) I extended Jay Fields' code by adding methods to combine error messages from different models:


3) An example Presenter, combining a User object and an Account object into a Preference presenter, is here:


4) An example controller, using the Preference presenter, is here:


Also, Jay wrote an excellent recipe for Advanced Rails Recipes that covers this technique.

Labels: ,

Thursday, January 3, 2008

Dynamic Content Caching Recipe

hello everyone,

Pragmatic Programmers just released an updated beta version of Advanced Rails Recipes which contains another recipe that I contributed, this one about dynamic content caching. I've built a few sites that had a lot of static content, with only bit of dynamic content on each page (usually a signout button, or an admin link). In these cases, I was able to use page caching with a little bit of Javascript that looks for a cookie in the client browser and alters the page accordingly. The book is shaping up great and I'm learning a lot by reading other recipes, so definitely check it out!

Labels: ,

Monday, December 10, 2007

Advanced Rails Recipes in Beta

I contributed an acts_as_ferret recipe to a forthcoming Rails book, Advanced Rails Recipes, which is now available as an online Beta. Check out my publishing quasi-debut!

Labels:

Wednesday, November 28, 2007

Testing Rails SSL Requirements on Your Development Machine

I am building a Rails app that requires some portions of the site to use HTTPS, so naturally I'm using the SSL requirement plugin. The plugin works great, but if you're using Mongrel or WEBrick running out of script/server in your development environment, you now won't be able to talk to those parts of your site (since these servers do not include SSL encryption).

The solution is pretty easy, but it's not something I found written up elsewhere, so I thought I'd document it here. All you have to do is install your own local Apache server and have it proxy requests to the Mongrel or WEBrick instance, similar to how you would set up your production environment. For simplicity I didn't use a cluster of mongrels, or mod_balance, or anything like that, just a straight-through proxy. See "A Simple Single Mongrel Configuration" on the Mongrel site for details.

But there's a bit more you need to do in order to make things work with Rails and the SSL requirement plugin.  Below is a subset of my httpd.conf file; for clarity, I cut out all the default settings and just left what I added or changed.
Listen 80 # included in the default config
Listen 443 # Apache needs to know you want to accept connections over HTTPS

SSLCertificateFile /usr/local/apache/conf/newcert.pem
SSLCertificateKeyFile /usr/local/apache/conf/newkey.pem

# Below is optional, but was helpful to me in debugging this setup
CustomLog logs/ssl_request_log "%t %h %{SSL_PROTOCOL}x %{SSL_CIPHER}x \"%r\" %b"

<VirtualHost *:80>
ServerName localhost
ServerAlias 127.0.0.1

ProxyPass / http://localhost:3000/
ProxyPassReverse / http://localhost:3000
ProxyPreserveHost on
</VirtualHost>

<VirtualHost *:443>
SSLEngine On
ServerName localhost
ServerAlias 127.0.0.1

ProxyPass / http://localhost:3000/
ProxyPassReverse / http://localhost:3000
ProxyPreserveHost on
RequestHeader set X_FORWARDED_PROTO 'https' # don't forget this line!

</VirtualHost>


<IfModule ssl_module>
SSLRandomSeed startup builtin
SSLRandomSeed connect builtin
</IfModule>
What you're doing is setting up two different proxies through Apache to your Mongrel server; one via port 80, unencrypted, and one via port 443, encrypted with SSL.  If you don't include the X_FORWARDED_PROTO line in your 443 virtual host, Rails won't know that it's using SSL and the SSL requirement filter will fail.

The two SSLcertificate directives refer to the cryptographic data needed to encrypt the traffic. I just made some self-signed certificates (which are free).  There are a million tutorials out there; I used the one on Apple's site.  You'll need to pick a passphrase to protect your private key.  Pick something short (since this isn't the production site) because you'll need to enter it every time you want to reboot your server.

Start things up and you're ready to go!


wymanpark~ $ sudo httpd -k start
Apache/2.2.6 mod_ssl/2.2.6 (Pass Phrase Dialog)
Some of your private key files are encrypted for security reasons.
In order to read them you have to provide the pass phrases.

Server localhost:443 (RSA)
Enter pass phrase:

OK: Pass Phrase Dialog successful.
wymanpark~ $

Labels: , ,

Sunday, October 28, 2007

Better Capistrano Rewrite Rule for Maintenance Page

Capistrano comes with a nice little tasked called deploy:web:disable that puts up a maintenance page on your site. Assuming your webserver detects the presence of this page and rewrites any requests to display that page, this effectively disables your site. Very handy.

I found that the common configuration available on the Internet didn't work for me. Specifically, the Apache Rewrite rule needs to reference DOCUMENT_ROOT. Here are my rewrite rules; remember that for this to work, they have to be first:

RewriteCond %{DOCUMENT_ROOT}system/maintenance.html -f
RewriteCond %{SCRIPT_FILENAME} !maintenance.html
RewriteRule ^.*$ %{DOCUMENT_ROOT}system/maintenance.html [L]

Labels: ,

Thursday, October 25, 2007

acts_as_solr, Capistrano, and testing

Deployment

I've recently deployed acts_as_solr and the Apache Solr server for full text searching in a new app I am building. The plugin works great, is easy to setup, but can cause some havoc with your deployment strategy if you don't pay attention.

For one thing, out of the box the plugin stores volatile information (tmp files, the indices used for searching, and log files) within the plugin itself, because it comes with its own version of Solr pre-installed. That makes it easy to get started and play with Solr but is not acceptable for a production environment (because every time you redeploy your code you'll be destroying and re-creating those directories within the plugin).

The Java Underworld

Unfortunately this means you'll have to descend from the mighty, glistening tower of Ruby into the Java underworld to setup a Tomcat servlet running Solr. This tutorial does a great job of explaining the process which is pretty easy.

Starting and Stopping Solr

I also have added some Capistrano tasks to start and stop the Solr server during deployments to lessen the chance that the index will be corrupted:
namespace :solr do

task :start, :roles => :app do
run "cd #{latest_release} && #{rake} solr:start RAILS_ENV=production 2>/dev/null"
end

task :stop, :roles => :app do
run "cd #{latest_release} && #{rake} solr:stop RAILS_ENV=production 2>/dev/null"
end

task :restart, :roles => :app do
solr.stop
solr.start
end

end
For Capistrano to be able to run the above tasks, I also had to patch the start:solr rake task that comes with acts_as_solr to use backticks instead of exec:

`java -Dsolr.data.dir=solr/data/#{ENV['RAILS_ENV']} -Djetty.port=#{SOLR_PORT} -jar start.jar`

Testing

acts_as_solr will greatly slow down any tests that use fixtures (because the indices get updated every time the fixture data gets added or removed). Ideally you would mock the Solr server in unit and functional testing (unless you're directly testing your app's interaction with Solr), but I haven't found a way to mock Solr before your tests run, when fixtures are being added. One way to go might be to create something like mailtrap for Solr requests. The other thing that might help is installing a modification of Solr called background-solr which defers all the index save and destroy requests and performs them in batches -- the idea being that during testing, maybe you would just avoid calling that batch update method for most of your tests, except where you are testing interaction with Solr (as in an integration test).

Building the Index

This is really great rake task to help build the Solr index. It's much faster and more effective that doing it manually for each of your models.

Labels: , ,

Thursday, September 20, 2007

random_data gem released

I just released my first Ruby gem. I have a library of functions that I use for generating realistic data so I can have more meaningful examples to work with during development. So I used the newgem generator and hoe to package it all into a gem called random_data.

It provides a Random singleton class with a series of methods for generating random test data including names, mailing addresses, dates, phone numbers, e-mail addresses, and text. This lets you quickly mock up realistic looking data for informal testing.

Instead of:
>> foo.name = "John Doe"
You get:

>>
foo.name = "#{Random.firstname} #{Random.initial} #{Random.lastname}"
>> foo.name
=> "Miriam R. Xichuan"


The gem also includes code for phone numbers, e-mail addresses, physical addresses, and (primitive) text generation.

You can install it via:

sudo gem install random_data
For more details and full documentation, visit the rubyforge site.

Labels: ,

Sunday, August 26, 2007

Cache Test Dummy

I use page caching in a lot of my projects, but could never figure out how to test them properly. There's a pretty good plugin that does this but I could never get it to work right. Until today, when I noticed that caching is turned off in testing mode! No wonder I couldn't test it!

So, it seems obvious now, but it never occurred to me to change this line in my test.rb file:

config.action_controller.perform_caching = true


Note that you will have to make sure your pages get swept properly after running your tests. I have a rake task that deletes all of the files, I think I got it from the Peepcode caching episode:


desc "Delete all cached files"
task :sweep_cache => "tmp:cache:clear" do

%w(index.html shows* about.html audio.html contact.html photos.html photos/*.html).each do |pattern|

rm_rf(Dir[File.join(RAILS_ROOT, "public", pattern)])

end

end



A little clunky, but it works. I might someday come up with a way to invoke all of my sweepers instead so I don't have to manually add all of those filenames. I think call this task from an autotest hook:




[:red, :green].each do |hook|
Autotest.add_hook hook do |at|
`rake sweep_cache` if File.exist?("#{FileUtils.pwd}/lib/tasks/cache.rake")
end
end



Because the plugin doesn't have many examples, here's a couple of my tests:
caching_story_test.rb



class CachingStoryTest < ActionController::IntegrationTest
fixtures :all

def test_page_caching
assert_cache_pages(about_path, contact_path)
end

def test_storyteller_caching
storyteller = storytellers(:storyteller_show_1_id_1)
assert_cache_pages(show_storyteller_path(:id => storyteller.id, :show_id => storyteller.show.id))
end

end



sweeping_story_test.rb


class SweepingStoryTest < ActionController::IntegrationTest

fixtures :all
include LoginLogoutDSL
include StoopDefaults

def test_should_sweep_news_items

logs_in
news_item = news_items(:double_whammy)

assert_expire_pages(news_items_path, news_item_path(news_item), index_path) do |*urls|
put news_item_path(:id => news_item.id), :news_item => @@news_item_default_values
end

assert_expire_pages(news_items_path, index_path) do |*urls|
post news_items_path, :news_item => @@news_item_default_values
end

assert_expire_pages(news_items_path, news_item_path(news_item), index_path) do |*urls|
delete news_item_path(:id => news_item.id)
end

end
end


(I'm sure there's a way to DRY up that last test, but I found it much harder to figure out which test was failing when I did that.

Last note: the expiration test only work with path routes, not url routes. So you call "delete news_item_path(...)" vs "delete news_item_url(...)".

Labels: ,

Tuesday, August 21, 2007

Roll your own podcast feed with Rails and Ruby's RSS library

I recently launched a new Rails app that powers Stoop Storytelling which was quite a labor of love. One thing I discovered was that if you take the time to design your models carefully, it's easy to add powerful capabilities later. Since The Stoop has a large trove of mp3 audio files containing all of the stories told during the series, I thought it would be fun to build a podcast. It turned out to be fairly easy although there are a few gotchas.

Shoutout

I was inspired by the incomparable Topfunky's podcast application:
http://svn.topfunky.com/podcast/

I've added code to manage the iTunes metadata that extends the original RSS standard.

Ingredients

You need to use the RSS library that comes with that standard Ruby library, but if you want to add iTunes metadata, you'll need to update the standard library like so:
cd /usr/local/src/
wget http://www.cozmixng.org/~kou/download/rss-0.1.9.tar.gz
tar zxvf rss-0.1.9.tar.gz
cd rss-0.1.9
ruby setup.rb
sudo ruby setup.rb
Then you need to require the right modules in a Rails initializer file. Mine is config/initializers/require.rb:
require 'rss/2.0'
require 'rss/itunes'
Model

Like Topfunky, I found it easier to include the bulk of the function in the model. You could also build this up using a Builder template, which might be easier to test, since you can use assert_xml_select to test it. But my philosophy was generate it in the model, test it as best I could with a unit test, then test the output completely in my controller test.


def self.rss

author = "Overall Podcast Author/Artist"

rss = RSS::Rss.new("2.0")
channel = RSS::Rss::Channel.new

category = RSS::ITunesChannelModel::ITunesCategory.new("Arts")
category.itunes_categories << \
RSS::ITunesChannelModel::ITunesCategory.new("Literature")
channel.itunes_categories << category

channel.title = "Podcast Title"
channel.description = "Podcast description, can be a paragraph"
channel.link = "http://www.example.com/"
channel.language = "en-us"
channel.copyright = "Copyright #{Date.today.year} I Own This"
channel.lastBuildDate = Audio.last_modified.updated_at
# the above uses a method I built on the Audio model that finds
# the last modified file and makes that the build date for the
# whole podcast channel

# below is your "album art"
channel.image = RSS::Rss::Channel::Image.new
channel.image.url = "http://www.example.com/images/app_rss_logo.jpg"
channel.image.title = "Same as podcast title"
channel.image.link = "Should be same as link for whole channel"

channel.itunes_author = author
channel.itunes_owner = RSS::ITunesChannelModel::ITunesOwner.new
channel.itunes_owner.itunes_name=author
channel.itunes_owner.itunes_email='info@example.com'

channel.itunes_keywords = %w(Common Misspellings of Key Words)

channel.itunes_subtitle = "This appears in the description column of iTunes"
channel.itunes_summary = "This appears when you click the 'circle I' button in iTunes"

# below is what iTunes uses for your "album art", different from RSS standard
channel.itunes_image = RSS::ITunesChannelModel::ITunesImage.new("/path/to/logo.png")
channel.itunes_explicit = "No"
# above could also be "Yes" or "Clean"

Audio.find(:all).each do |audio|
item = RSS::Rss::Channel::Item.new
item.title = audio.title
link = "http://www.example.com/#{audio.public_filename}"
item.link = link
item.itunes_keywords = %w(Keywords For This Particular Audio Clip)
item.guid = RSS::Rss::Channel::Item::Guid.new
item.guid.content = link
item.guid.isPermaLink = true
item.pubDate = audio.updated_at

description = "Long description of this particular audio file, appears in circle I section of
iTunes"

item.description = description
item.itunes_summary = description
item.itunes_subtitle = audio.nice_title
item.itunes_explicit = "No"
item.itunes_author = author

# TODO can add duration once we can compute that somehow

item.enclosure = \
RSS::Rss::Channel::Item::Enclosure.new(item.link, audio.size, 'audio/mpeg')
channel.items << item

end

rss.channel = channel
return rss.to_s

end


Easy, right? What you're doing is building up an RSS feed that complies with the RSS 2.0 standard and with Apple's iTunes extension to the base standard. The category code is a little tricky and there may be a better way to do it. I found the RSS library docs a bit hard to understand.

Controller

All of the heavy lifting is done for us, so the controller is easy.

class AudioController < ApplicationController

def index
respond_to do |format|
format.html { @audio = Audio.find(:all) }
format.xml { render :xml => Audio.rss }
end
end

end

Route

Because of my inflection rules (where audio is singular and plural) I can't use the baked-in REST route to handle this situation, so for this to work I needed to manually wire the following route:

map.formatted_audio 'audio.:format', :controller => 'audio', :action => 'index'

Unit Test

I haven't written enough tests for this, but here's a start so I at least know the right number of audio files are in there. In audio_test.rb:

def test_should_generate_audio_rss
assert_equal Audio.count, Audio.rss.scan(//).size
end

Functional Test

As mentioned earlier, testing the method here gives you the added benefit of assert_select, but you need to apply Jamis Buck's wizardry to get it to work with XML files by creating an assert_xml_select method.

I'm testing almost the exact same thing as the unit test, but at least I know the file gets served up the way I expect it, and now I'm also testing whether the items have been nested properly within the channel.

def test_should_get_audio_rss
get :index, :format => 'xml'
assert_response :success
assert_xml_select 'channel item', :count => Audio.count
end


I want to improve this by incorporating XML validation into my tests but haven't figured that out yet. For now, I'm validating the feed itself.

Feed Validation

The last step before submitting to iTunes is to make sure this feed works and is valid XML. I used FeedValidator.

iTunes Submission

Pretty straightforward. Just follow Apple's directions. Getting the category right was a little tough, and there's also an issue with iTunes not recognizing the feed image properly. Doesn't look like they've fixed it yet.

Submitting Elsewhere

I added the following to the head section of my app's layout to help other aggregators find the feed:


%link{:rel=>"alternate", :type=>"application/rss+xml", :title=>"Stoop Storytelling Podcast", :href => formatted_audio_path(:format => 'xml', :only_path=>false)}


This reference was helpful in constructing the above tag.

Advertising the Link

Finally, I posted links to the iTunes store on the site itself. If people have iTunes or another podcast client installed, clicking on the store link will cause them to subscribe and help boost the rating of the podcast (thus, you don't want to give people the link to the audio.xml file itself so direct subscriptions won't get counted by Apple).

Final Product

You can listen to the podcasts here:
http://www.itunes.com/podcast?id=262444919

References

Beyond the references already cited, I relied heavily on the RSS library tutorial and reference.

Future

Is there interest in a plugin to help streamline this process further?

Labels:

Saturday, August 18, 2007

REST, Caching, and Apache Rewrite problems SOLVED

Problem: Your Rails app is working beautifully in development and test mode, but for some reason, on the production server, users intermittently have trouble POSTing data. It seems like Apache is turning POST requests into GET requests. Looking at the Apache log, you see the POST request come in. Looking at the Rails production log, you see it has been rewritten as a GET request, triggering a "show" or "index" action in your RESTful controller. This may have started when you implemented page caching, but you're not sure. What gives?

Solution: This is a clash between page caching and the Apache rewrite rules. Apache sees that there is a directory in your RAILS_ROOT/public/ directory that matches the name of the URL you are POSTing to, and tries to satisfy the request by serving up content from that directory. You need to change your rewrite rules so that only GET requests for URLs are satisfied with a static file, and all POST requests are delivered to your rails app (to dispatch.fcgi, your mongrel cluster, etc.)

Solution 2: Even if you're not using caching, this problem could also occur if you have a directory in your public/ directory that matches a route in your app (so if you have a route for /assets/, and there's a RAILS_ROOT/public/assets/ directory, you may have the same problem.

Details:

1) Change this following rule in your .htaccess or your Directory directive:

RewriteCond %{REQUEST_FILENAME} !-f

to this:

RewriteCond %{REQUEST_FILENAME} !-f [OR]
RewriteCond %{REQUEST_METHOD} !^GET*

2) I'm not 100% sure this next step is necessary, but I couldn't get it to work otherwise. Add the following directive, IF the caveats in the mod_dir documentation don't apply:

DirectorySlash Off

These two steps will tweak the way Apache views incoming URLs and delivers them to your page_cached, RESTful Rails app.

Other Complications

This bug is hard to track down because it's intermittent, happening only when there are cached pages or a cached directory. On the site where I had this problem, I had a Capistrano task that swept my cached pages whenever I redeployed the app. So you can imagine my agony when I'd make a change, redeploy, the app would work great, then an hour later when pages had been cached, everything stopped working. Argh!

References

This is about 20 hours of my life I'll never get back, so I hope this saves you some pain. The following references were extremely helpful in devising this solution:

Rails mailing list post #1
Rails mailing list post #2
Mephisto wiki

Labels:

Wednesday, May 30, 2007

Functional Testing for Attachment-fu

I found a lot of good tutorials on how to setup attachment_fu (which lets you easily store images or other binary data in a Rails app, either in the filesystem or in the database itself), but none of them explain how to write proper functional tests. Since a lot of other people have been posting this question, I thought I would post what I just got working:

def test_should_create_new_attachment

fdata = fixture_file_upload('/files/photo1.jpg', 'image/jpeg')

login_as :bob

assert_difference Photo, :count, 2 do
post :create, :photo => { :uploaded_data => fdata }, :html => { :multipart => true }
end

assert_redirected_to user_url(users(:bob))
assert_valid assigns(:photo)

end

The above assumes you create a 'files' directory inside of your fixtures directory. Also note that if you have thumbnailing enabled, each file you upload will create multiple attachment objects (in this case, one to represent the original image and one to represent a thumbnail).

If you're curious, here's how I have the plugin configured:

class Photo < ActiveRecord::Base

has_attachment :content_type => :image,
:storage => :file_system,
:max_size => 500.kilobytes,
:resize_to => '320x200>',
:thumbnails => { :thumb => '100x100>' },
:processor => 'ImageScience'

validates_as_attachment


end

Labels: ,

Sunday, May 20, 2007

Rcov is awesome but not without these tweaks

I've recently started using the rails_rcov plugin to test how well I am testing the code I am writing. It's been a great help, but I was getting very strange results until I read this post about how to configure rcov properly to concentrate its testing. (Basically you have to force it to run your unit and functional tests in isolation which seems counterintuitive). It's easy enough to add parameters to your rakefile that will straighten it out.

Now I am going to try to get Heckle working for even better testing of my tests.

Labels: ,

Tuesday, May 15, 2007

Build a Toy First

So far in my career I've only developed web applications for internal use -- intranet sites and the like. Recently I gave myself the challenge to create a simple public web app over the course of the weekend. Writing the code took only the weekend, but getting the deployment configuration right, and optimizing to withstand just the small trickle of traffic I was seeing took two more weeks.

I thought I knew Rails and Apache and MySQL and other technologies inside and out, but once I had to really cope with real-world obstacles I was knocked on my ass. My original configuration was not fast enough, and I had to implement caching to make things more responsive. I set up a number of monitoring systems to automatically restart my dispatchers if something went wrong. I found an awesome SSH client for my Treo which I had to use when my server started acting up and I was away from my home computer.

I also learned that the Internet is international -- I unconsciously assumed that most of my users would be based in the United States, but that's exactly wrong. I've got users from Växjö to Santiago de Compostela.

The site itself isn't much to look at -- I'm no graphic designer -- but it represents a major lesson for me in how to deploy a public-facing site. I'm greatly influenced by it while pursuing my other projects -- for instance, I now bake caching considerations directly into my code. I am not relying on flash[:notice] to pass messages back to the user. I'm either creating separate notification pages that can be cached, or I'm using Ajax to just update small parts of the page with status messages.

Labels: ,

Sunday, April 22, 2007

Taming assert_select with css_select and Regexp.escape

I had a lot of trouble writing a test to check whether all elements of an array were showing up in a table properly; I needed more fine-grained testing than assert_select was allowing me. I needed to guarantee that every element in an array could be found as a child of a particular table.

Fortunately, I noticed that Rails also has a css_select method which returns an array of selected elements without running tests on them. This lets you run your own tests using the results of the selection.

This isn't the prettiest code in the world (I'm also testing a formatting helper, hence the literals '$200.00' and so on), so I'll probably refactor it later. There might also be a way to do this more elegantly with assert_select. But I think this does show the utility of css_select.
elements = css_select('table#investors tr td')

[[investors(:investor_8),'$200.00'],
[investors(:investor_9),'$343.40']].each do |i,amount|

assert_not_nil elements.detect \
{ |e| e.to_s =~ /#{i.name}/ }

assert_not_nil elements.detect \
{ |e| e.to_s =~ /#{Regexp.escape(amount)}/ }

end

Things to watch out for:
  • You need to force the conversion of the element to a String for the regular expression comparison (with to_s)
  • In this particular example, since I am using string literals with special characters, I had to use Regexp.escape to make $ and . match properly

Labels: ,

Wednesday, March 14, 2007

Pitfalls of acts_as_ferret, with DrbServer to the rescue

I don't know how general this case is, but I've found that if you want to use acts_as_ferret in your production Rails app, you need to run your Ferret server as a separate DrbServer process.

Background

acts_as_ferret gives you a powerful search capability, and it's much easier to implement than MySQL full text querying.

Numerous tutorials (here and here) describe how to do this. In development mode, it's as simple as adding something like this to your model:

acts_as_ferret :fields => [ :city, :state ]

and this to your controller:

@users = User.find_by_contents(params[:keywords])

Problem

I started getting all kinds of corrupted index errors when I put this into production, because acts_as_ferret can't handle multiple separate processes access the index at once. This caused a lot of scrambling as I was starting to get a lot of traffic.

Solution

Fortunately I discovered that acts_as_ferret comes with its own built-in DrbServer. As soon as I set that up, everything worked great.

The developer does a great job explaining what to do. You add :remote => true to your acts_as_ferret declaration, setup a ferret_server.yaml config file, then run:

RAILS_ENV=production script/runner vendor/plugins/acts_as_ferret/script/ferret_server

...and you're golden.

UPDATE: To make the acts_as_ferret unit tests run correctly, add the following line to your test/environment.rb file:

AAF_REMOTE = true

Labels: ,