Tag: Ruby on Rails
Screencasting! It’s the teaching medium that’s sweeping the nation!
I love screencasts. Certain ones, anyway. Done well they enrich understanding. Done wrong they’re a snore and a half. I’m in the middle of redesigning Jetrecord and the more I redesign the more I’m considering redoing it from the ground up (and doing some screencasts about the process).
Why? Well, I’m looking at all the shortcuts I’ve taken to get to where Jetrecord is today and frankly, it’s embarrassing. Don’t get me wrong. I LOVE Jetrecord and I think it’s both a great idea and a great product. I just think I can do better. I owe it to my customers to make it the best it can be.
I’m also one of those people who loves great foundations; well, I love the idea of great foundations. I love talking about foundations, dreaming about them, planning them, scheming. But when it comes time to build the application I get so focused on delivering a great experience and a great product that I glance right over the possibility that I may change my mind on a few foundational things, that people involved in the project come and go, that features come and go, et cetera. I know it’s impossible to plan for everything but I know there are some things I could do a whole lot better.
When I build an application I put a whole lot of time into building great user experiences (I try, anyway) and just enough time into making sure the application keeps chugging along. I tell myself, “it’s not a problem until it’s a problem,” which is a great way of looking at life for most matters. 80% of the time, we really don’t need to plan for all the contingencies we plan for. Of course, it’s that scary 20% that keeps us awake at night with a death grip on our Blackberries waiting for that 3am email from the error system. (Thanks, Internet!) An example of a 20% problem would be the current financial crisis in America. We were doing fine ignoring the underlying foundational problems for a while but now it looks like we picked the wrong year to stop sniffing glue. So when it’s a problem, it’s a real big, fat, hairy problem, with mutated teeth growing out of a tumor in your stomach.
Granted, most of us don’t encounter these problems. For small, local, mostly internal applications, applications in which we and maybe 20 other people are the only users, the 20% problems can usually be contained. It’s when we move into the realm of needing to support thousands of users in multiple countries on a 24/7 basis that we start encountering the mutant teeth kind of problems.
Jetrecord isn’t quite the super-multinational app yet but I believe it’s going there. It will, I tell you!!! Moreover, I’m at a place in its development where stopping and doing the right thing makes sense. I’m redesigning anyway and my plans already involve a better foundation. If I wait until it actually is supporting 20,000 users in 20 countries, I’ll have quite a job on my hands. I’ve racked up some code debt and it’s time to pay it off–and start saving a little.
But why do a screencast about it?
For one thing, I’ve been a code leech for most of my career. I’ve written a couple free things here and there and released them into the wild but nothing too substantial. Mostly I’ve read other people’s books and articles, attended other people’s conferences, and taken other people’s free code samples. And truly, all of these things have taught me a lot and I wouldn’t be where I am without the generosity of many, many people. But I think it’s time I did something substantial, extraordinary, and excellent for others.
Second, it’s going to force me to write the best code I can. I’m embarrassed during code reviews if there are glaring mistakes so I’m going to try hard to make sure it’s worthwhile code because it’s public. I already know I’m going to have to do a lot more research to find best practices and I’m sure I will make mistakes and I hope someone will be kind enough to suggest improvements. This will benefit me and you as coders and ultimately it will benefit the applications we build and the clients and the users we support. Win-win-win-win-win.
Lastly, I’ve been doing screencasts since 2001, since before they were called screencasts. In one of my previous jobs we were publishing “help videos” in Camtasia on our web site to show users how to use the site and some of the software products we supported. I consider myself knowledgeable enough to make a good video, or at least, to give it a go. Whether they’re actually good or not is up to you.
Having said all of that, I still haven’t fully settled this in my mind. I want to do it. I know it would be good. However, screencasts take a lot of time to do right. For every minute of production video there’s 30+ minutes of material and mistakes that don’t make it in, not to mention planning, outlines, scripts, research, creating or finding other media like music, artwork, and extra video. Then there’s editing, producing, and finding a place to host it. It’s not unlike being a T.A. in grad school. You’ve got this whole other full-time teaching job to do while you’re trying to complete your own studies. (At least, that was my experience.) It’s both fantastic and super-sucky at the same time. And you don’t get paid!
So, if the series is going to show up, it’s going to show up here on this web site, and probably very soon because I’m already building the foundation for the next Jetrecord (which is going to rock, by the way; if you’re a pilot, you should use it). If I wait too long, I definitely won’t do it.
Okay, thank you, and now for my next anti-climax…
This is a slight modification of code originally written by Alastair Brunton. I recently implemented this for Jetrecord and since Alastair was so generous, I decided to share the love as well. I have changed Alastair’s code to generate a sitemap index file plus sitemap files for each model, all of them gzipped to save on bandwidth.
I have also added Capistrano code to copy sitemap files from the previous release to the current release so we don’t lose our sitemap files when we deploy a new release.
Remember, Google sitemaps are for publicly available URLs. They’re for pages that you want Google to find and index. If you don’t want Google to find your CIA Operatives records, don’t tell Google about it!
Let’s just go straight to the code. I am going from the top down in my application’s root directory.
app/models/your_model.rb
You must add this code to each model that you want to generate a sitemap for. Here is an example for Airports on Jetrecord.
# put this inside app/models/airport.rb
def self.get_paths
path_ar = []
self.find(:all).each do |model|
path_ar << {:url => "/airports/#{model.to_param}", :last_mod => model.updated_at.strftime('%Y-%m-%d')}
end
path_ar
end
config/sitemap/sitemap_tasks.rb
This is for Capistrano. You probably don’t have a config/sitemap directory. I created one and put my Capistrano sitemap task in it. This tells Capistrano, “After deploying my new release, copy the sitemap files from the previous release and store them in the same location in the current release.”
Capistrano::Configuration.instance(:must_exist).load do
namespace :sitemap do
desc "Copy the sitemap files after deploy"
task :copy_sitemap, :roles => :app do
puts "copying Rails sitemap files"
sudo "cp #{previous_release}/public/sitemaps/* #{current_release}/public/sitemaps/"
end
after :deploy, 'sitemap:copy_sitemap'
end
end
config/deploy.rb
This file usually contains your typical Capistrano recipes. All you have to do is require the sitemap_tasks file we created above.
# At the top of the file, after any other required files
require 'config/sitemap/sitemap_tasks'
lib/google_sitemap.rb
This is the meat of the whole thing. Kudos to Alastair for setting this up. The reason I modified it into using a sitemap index with sitemaps for each model is because Google allows a total of 50,000 links per sitemap. I have 48,000 navigation fixes, 20,000 airports, and 3,000 navaids in Jetrecord. By necessity I have to split my sitemap into many sitemaps.
I’m also gzipping the sitemap files because Google can read them and it saves bandwidth. Oh, and the URL to ping Google has changed, as has the XML namespace for their sitemap tags.
require 'net/http'
require 'uri'
# A class specific to the application which generates a google sitemap from the contents of the database.
# Author: Alastair Brunton
# Modified: Harry Love 2008-06-09
class GoogleSitemapGenerator
def initialize(base_url, sources)
@base_url = base_url
@sources = sources
end
# 1. Iterate through each model's #get_paths method
# 2. Create sitemap file for each model
# 3. Create sitemap index file
# 4. Ping Google
def generate
path_ar = []
sitemaps = []
@sources.each do |source|
# initialize the class and call the get_paths method on it.
path_ar = eval("#{source}.get_paths")
xml = generate_sitemap(path_ar)
save_file(source, xml)
end
index = generate_sitemap_index(@sources)
save_file('index', index)
update_google
end
# Create a sitemap document for a model
def generate_sitemap(path_ar)
xml_str = ""
xml = Builder::XmlMarkup.new(:target => xml_str)
xml.instruct!
xml.urlset(:xmlns => 'http://www.sitemaps.org/schemas/sitemap/0.9') {
path_ar.each do |path|
xml.url {
xml.loc(@base_url + path[:url])
xml.lastmod(path[:last_mod])
xml.changefreq('weekly')
}
end
}
xml_str
end
# Create a sitemap index document
def generate_sitemap_index(sitemaps)
xml_str = ""
xml = Builder::XmlMarkup.new(:target => xml_str)
xml.instruct!
xml.sitemapindex(:xmlns => 'http://www.sitemaps.org/schemas/sitemap/0.9') {
sitemaps.each do |site|
xml.sitemap {
xml.loc(@base_url + "/sitemaps/sitemap_#{site}.xml.gz")
xml.lastmod(Time.now.strftime('%Y-%m-%d'))
}
end
}
xml_str
end
# Save the xml file (gzipped) to disk
def save_file(source, xml)
File.open(RAILS_ROOT + "/public/sitemaps/sitemap_#{source}.xml.gz", 'w+') do |f|
gz = Zlib::GzipWriter.new(f)
gz.write xml
gz.close
end
end
# Notify Google of the new sitemap index file
def update_google
sitemap_uri = @base_url + '/sitemaps/sitemap_index.xml.gz'
escaped_sitemap_uri = URI.escape(sitemap_uri)
Net::HTTP.get('www.google.com', '/webmasters/tools/ping?sitemap=' + escaped_sitemap_uri)
end
end
lib/tasks/sitemap.rake
This is the rake task that we’ll call periodically from Cron to generate new sitemap files.
require 'google_sitemap'
namespace :google_sitemap do
desc "Generate a Google sitemap from the models"
task(:generate => :environment) do
# Generate sitemaps for each of the models listed in the array
sources = %w( Airport Navaid Fix AnotherModel AnotherModel AndAnotherModel EtCetera )
sitemap = GoogleSitemapGenerator.new('http://yourdomain.com', sources)
sitemap.generate
end
end
public/sitemaps
Assuming this directory doesn’t exist already, create it.
Also, depending on what stack you’re using to deploy your Rails app, you may also need to tell your server to skip proxying HTTP requests to this directory. For example, I’m proxying requests to Mongrel via Apache. So, in the Apache virtual host conf file for my app, I had to add a ProxyPass directive so Apache would serve the sitemap files instead of Mongrel.
# Right after the ProxyPass directives for images, stylesheets, and javascripts
ProxyPass /sitemaps !
Don’t forget to restart Apache after you save the new conf file!
Add a Cron Job
Lastly, you need to add a cron job to call the rake task so we can generate new sitemap files from time to time. The frequency is up to you and the requirements of your app.
Unfortunately, I’m not up to date on raw Cron commands. I use a GUI provided by my web host. But here’s the command I’m using on Solaris to call the rake task. You’ll have to edit this to suit the specifics of your application and server environment.
cd /var/www/apps/myapp/current && /opt/local/bin/rake RAILS_ENV=production google_sitemap:generate
Don’t forget to tell Rake to use the production environment. Another potential gotcha: you usually have to give cron the full path to rake. You can find out where it is on your server by logging in as the user you plan to use for the cron job (usually root) and doing “which rake”. If that doesn’t bring it up it means rake isn’t in your PATH. That’s okay. You’ll just have to do a little more digging to find out where rake is installed on your system.
If I’ve left out anything let me know. By the way, this would make a great plugin or gem, if only I knew how to make them.
Using the new config.gem method in Rails 2.1? Using the Twitter4R gem to interact with Twitter in your Rails application? Make sure you add the :lib attribute, otherwise ruby-gems won’t know which gem you’re talking about.
In your environment.rb file:
config.gem 'twitter4r', :lib => 'twitter'
Skip to the code
Update: December 18, 2007: less code, same result
Summary
If you use the Restful Authentication plugin with the –include-activation and –stateful options (see Acts_As_State_Machine and the post by Jonathan Linowes which united the two plugins), you’ll need to make a couple modifications to the code after you run the generator in order to keep the user activation email from being sent simultaneously with the user signup notification email.
Problem
The UserObserver code sends an activation email during the after_save callback if the user’s state is “pending.” Without activation the state transition goes from pending to active and we can leave the code alone. With activation, however, we introduce another state: the “state of being notified that our account has been created.”
If we change the UserObserver to watch for an “active” state instead of “pending,” the user will receive an activation email every time we save the user’s record and that puts the user into the state of “I’m about to adios this annoying website.”
Suggested Solution
We add a temporary state “notified” between pending and active that allows us to send the activation email at the right time with little disruption to the original restful authentication code.
Just before we “activate!” the user in the UserController, we “notify!” them, putting them in the “notified” state, save the user, which calls the after_save callback, which sends the activation email, and then return control to the UserController, which calls “activate!,” putting the user in the “active” state.
The Code
Changes to User.rb
Update: I removed the :do_notify code which was redundant (state transitions automatically save the record)
state :notified
event :notify do
transitions :from => :pending, :to => :notified
end
event :activate do
transitions :from => :notified, :to => :active
end
event :suspend do
transitions :from => [:passive, :pending, :notified, :active], :to => :suspended
end
event :delete do
transitions :from => [:passive, :pending, :notified, :active, :suspended], :to => :deleted
end
Changes to User_Observer.rb
def after_save(user)
UserMailer.deliver_activation(user) if user.notified?
end
Changes to User_Controller.rb
# In the activate method,
# insert the notify! line right before the activate! line
current_user.notify!
current_user.activate!
If I’ve left something out, or if you have a better solution, please say so below. Cheers!

Client
University of Washington
Details
The Activity Report Form (aka The ARF) is an internal activity tracking application that I created for the librarians of the Health Sciences Library. The librarians need to see statistics, reports, and trends on the types of activities they engage in and the groups they interact with. The previous solution used a web based form from a third party provider that we customized. After capturing the data I was required to import the data into Excel each month and fiddle with the input and output to make it look the right way. Ugh!
Continue reading …