Friday, August 31, 2007

Everything I Need to Know About Improv I Learned in Camp

Recently BIG held our first summer improv camp for kids. For their final showcase performance, our instructors, Bridget Cavaiola and Chris Enoch, created two posters of hints posted on the wings of the stage. I thought they were pretty funny but also very apt.

Some of these tips are specific to working with kids in theater, and some are not as useful for adult improvisors (because you're often trying to get adults to be MORE like kids, while the teachers were trying to get the kids to be a bit more focused), but overall I think they are great advice. Sort of an "Everything I Need to Know About Improv I Learned in Summer Camp":


Do
  • Establish strong characters
  • Support your peers and make _them_ look good: team effort!
  • Allow yourself to be seen & heard
  • Set up your relationships and environment
  • Focus on an activity (but don't talk about it)
  • Stay focused on the show whether you're on _or_ off stage
  • Say "Yes...and..." - add info
  • Use details and specificity
  • Go with your instincts => don't second guess
  • Project your voice
  • Maintain your character (role, accent, etc.)
  • Jump...and the net will catch you
  • Keep up the energy
Don't
  • Ask questions...give gifts instead!
  • Talk over people or out of turn
  • Negate your partner
  • Leave the scene or kill partner
  • Pull props out of thin air
  • Talk or distract when off stage
  • Hestitate
  • Say our "list of words" [not a reference to profanity but to bad expository habits]
  • Eat or remove clothing on stage
  • Look at audience members - or Bridget!
  • Forget to hold for laughter

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(...)".

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?

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