Mailcar – A Rails bulk / mass email plugin

Plugins, Ruby on Rails 1 Comment »

In a nutshell, mailcar makes it (a little) easier to send a mass emails from your Rails app.

Get the plugin!

Newsletters, feature updates, or God forbid, security breach notices all require sending potentially thousands of emails. There are plenty of service providers who will do it for a fee, but if you’re a small-ish site run by stingy admins (like Obsidian Portal), then you don’t want to drop $100 every time you announce a new feature.

Mailcar is my attempt at simplifying this process a little and making it slightly fault tolerant. You can initiate a mass mailing, let it run for a while, stop it, resume it, and generally not worry about double (or infinite) sends or getting irreversibly interrupted.

In it’s current state (0.1) it does not work out of the box. You’ll need to edit the code directly to insert your own ActionMailer model, and add a function for extracting the list of email addresses. There’s probably other stuff too. I still wanted to post it because it’s a good starting point and the code is very simple, so you should have no trouble finding where to make modifications.

If anyone finds this plugin useful, please take a moment to refactor it and make it more reusable. Send me a pull request or a patch and I’ll put your name in the credits. Hopefully we can get it to a works-out-of-the-box state.

One final warning: sending thousands of emails without knowing what you’re doing will almost certainly land you on a spam blacklist. Double and triple check that your SMTP server is set up correctly with RDNS, SPF, HELO, and other acronyms that I don’t know. Don’t ask me about it because I don’t know. I’d just rather roll the dice on getting blacklisted than pay the bulk sender fees.

Boosh! My first Rails core contribution gets accepted!

Ruby on Rails 1 Comment »

I submitted my first Rails core contribution a while back, and I just happened to check in and see that it was accepted!

My first Rails core contribution!

Ruby on Rails 2 Comments »

I just submitted my first Rails core patch! It was only a small issue dealing with a fairly unusual case (MySQL SSL connections), but heck, every patch counts, right? That’s how open source works.

It hasn’t been included yet, so I’ve got my fingers crossed…Wish me luck!

Hacking the Ultrasphinx plugin to work with paginating_find

Plugins, Ruby on Rails No Comments »

If you’ve been following our blog you may have noticed that we’re using Solr and ActsAsSolr plugin for our searching.

Getting started with acts_as_solr
acts_as_solr for development and production in one Tomcat instance
Optimizing Solr and Rails – Index in the background

Unfortunately being Java, Solr is a bit of a memory hog. We’ve attempted all sorts of optimizations, but we’re going to take a leap and switch to Sphinx. Sphinx is a free open-source SQL full-text search engine.

First step is to get Sphinx itself installed. For that have a look at Rob’s post:
http://www.notch8.com/articles/2007/10/15/sphinx-and-ultrasphinx-and-eye-on-search

Ultrasphinx works pretty much out of the box with will_paginate; however, a lot of our work is currently compatible with paginating_find. So to keep rails memory footprint down by using one plugin we’re sticking with paginating_find. Unfortunately Ultrasphinx doesn’t work out of the box with paginating_find, so time for a little hacking…

Install paginating_find plugin:

piston import http://svn.cardboardrocket.com/paginating_find/paginating_find vendor/plugins/paginating_find

More information on paginating_find

Install ultrasphinx plugin:

piston import http://fauna.rubyforge.org/svn/ultrasphinx/trunk/ vendor/plugins/ultrasphinx

More information on Ultrasphinx

Now for hacking Ultrasphinx plugin to work with Paginating Find plugin. Piston is great for managing plugins; however, it can easily get confused if you’ve hacked a plugin directly. The solution: use a method called “Evil Twin” as mentioned on the Err the Blog: Evil Twin Plugin

Create a directory for the hack:

mkdir vendor/plugins/ultrasphinx_hacks
touch vendor/plugins/ultrasphinx_hacks/init.rb

Now edit the vendor/plugins/ultrasphinx_hacks/init.rb file with the hacks for paginating_find:

# Hack to allow Ultrasphinx to work with pagination_find
Ultrasphinx::Search.module_eval do
def first_page
1
end

def last_page
self.page_count
end

def previous_page?
previous_page ? true : false
end

def next_page?
next_page ? true : false
end
end

Now for adding a method to your controller for the search:

app/controllers/posts_controller.rb

def search
@query = h(params[:query])
begin
@posts = Ultrasphinx::Search.new(:query => @query,
:class_names => ‘Post’,
:page => params[:page] || 1, :per_page => 20)
@ posts.run
rescue RuntimeError
flash[:warning] = ‘Search is currently disabled. Please try again in a few hours.’
rescue NoMethodError
flash[:notice] = ‘No records found for this search’
end
render :action => ‘list’
end

app/views/posts/list.html.erb

    < % @posts.each do |cog| %>

  • < %= posts.name %>
  • < % end %>

< %= paginating_links(@posts) %>

Paginating End Result

Bonus : will_paginate styled links

Unfortunately paginating_find doesn’t automatically have the “style” that will_paginate automatically comes with. So to fix that just add a little css for the style and a partial for the pervious and next buttons.

.pagination{
padding: 2px;
}
.pagination a, .pagination a:visited{
padding: 0 5px;
border: 1px solid #9aafe5;
text-decoration: none;
color: #2e6ab1;
}

.pagination a:hover, .pagination a:active{
border: 1px solid #2b66a5;
color: #000;
}

.pagination .currentpage{
font-weight: bold;
padding: 0 5px;
border: 1px solid navy;
background-color: #2e6ab1;
color: #FFF;
}

.pagination .disablepage{
padding: 0 5px;
border: 1px solid #929292;
color: #929292;
display: inline;
}

And now for the partial:

app/views/shared/_paginate.html.erb

< % if (collection.is_a?(PagingEnumerator) || collection.is_a?(Ultrasphinx::Search)) && (collection.page_count != collection.first_page) -%>

< % end -%>

So in your code replace the code paginating_links call with a render partial:

# OLD VERSION
< %= paginating_links(@posts) %>
# NEW VERSION
< %= render :partial => ’shared/paginate’, :locals => {:collection => @posts} %>

Styled Paginating End Result

Resources:
ErrTheBlog: Evil Twin Plugin
Will Paginate Plugin
Paginating Find Plugin
Ultrasphinx
Sphinx

Before switching, ask your host for more

Business, Site Admin 1 Comment »

Before we switched to Slicehost, we had our VPS on Rimuhosting. I must say that I was very pleased with Rimuhosting’s service, and we never had any problems with stability. We eventually had to switch to Slicehost anyway because it was simply cheaper and we’re on razor-thin margins here.

Still, switching hosts is not a trivial process, and you have to factor in the switching effort when deciding if the price differential is worth it. If you’ve found a cheaper host and are thinking about jumping ship, the very first thing you should do is contact your current provider and see if they’ll match the new host’s price. That’s what we did initially with Rimuhosting. They didn’t match the deal, but they did give us a little more RAM for free. That tided us over for another month or two before we decided we wanted to give Slicehost a shot.

So, for posterity, and to help other bargain hunters, here is the letter I sent, and Rimuhosting’s response.

I’ve been a Rimuhosting customer for several months now. In general I am happy with the service, although my server seems to be getting more and more sluggish in recent weeks.

Due to the performance issues, I began shopping around for alternatives and I came across Slicehost ( http://www.slicehost.com/ ). Since they’re a competitor, I’m sure you’re already familiar with them.

According to their price list, they will give me 256MB RAM plus more disk space for $20/mo, compared to the 160MB of RAM I’m getting for $29. For me, the RAM is the big deal, and the extra storage is just icing on the cake.

I am seriously considering moving to slicehost, as I am finding that the 160MB I currently have is eaten quickly by Tomcat, mongrel, and MySQL. Plus, as I mentioned earlier, I have been seeing some very serious performance issues lately, with slowdowns on ssh as well as terrible response times from the web server. This could be due to not enough memory on my VPS, but I’m concerned it has to do with too few CPU cycles being devoted to the VPS.

In all honesty, I would rather not switch hosting companies, as I have been pleased with the level of service from Rimuhosting. In addition, moving a VPS installation is no small task and I do not relish the idea of trying to replicate all my settings on a new machine. Still, Slicehost’s pricing plan is very attractive.

Before I make any decisions, I would like to ask what sort of plan you would be willing to offer in order to compete with Slicehost. If you could offer a combination of more RAM and a lower price, it would go a long way towards tipping the scales in Rimuhosting’s favor.

Thanks for your time and I look forward to hearing from you.

Micah Wedemeyer

http://www.aisleten.com

Their response to me:

Hi Micah,

I’ve added some additional memory to your VPS:

[root@obsidianportal ~]# free -m
             total       used       free     shared    buffers     cached
Mem:           300        148        151          0         16         55
-/+ buffers/cache:         76        223
Swap:           95          0         95

Also moved your current pricing from 39.95, to where our current MiroVPS2 plans are, at 29.95 a month.

We feel that we offer a good value for the service we provide. We appreciate your business and hope you will stay with us.

attachment_fu + S3 + ruby tile cutter + Google Maps = Easy custom maps in Ruby on Rails

Plugins, Ruby on Rails 8 Comments »

For Obsidian Portal, we wanted the ability for users to upload their own maps. The simplest way to do this would be to allow them to upload an image, then display it statically. While not a terrible feature, it has some definite limitations.

What we really wanted was a way to allow users to upload maps and navigate around them as everyone is accustomed to with Google Maps. Luckily, the guys at Google make just such a thing possible. As they say, a picture is worth a thousand words, so here are some examples of what we’ve accomplished. I will add more as users begin uploading.

  • Kensing This is the main island for my D&D 3.5 campaign.
  • Caedwyr Isle. This is another D&D 3.5 campaign, run by one of our users.

Note: Sorry if any of those links are 404s. The users can delete the maps as they please.

Over the course of a 3-day weekend, I was able to go from nothing to everything. I had a lot of stumbling blocks, though, and with this tutorial it should go much faster.

So, without further ado, let’s get some background on the technology that drives Google Maps: tiles!

What’s a tile?

The data set for Google Maps is just a giant collections of 256×256 images called tiles. When you view a map in your browser, the smarts in the system determine what part of the Earth is visible on your screen and requests only the tiles for that portion. The tiles are then laid out in a grid pattern to make a seamless image. Moving the map around spawns off asynchronous requests back to the server to get more tiles. In this way, the maps load quickly, and allow unlimited scrolling.

By default, the map pulls the tiles from the main server at Google. This makes sense as most people using the API want to display actual images of the Earth, or the nicely made street maps that Google does. The clever guys at Google, however, made it possible to switch this up and request the images from anywhere, thereby allowing developers to serve up their own tiles, yet rely on Google Maps to lay them out and stitch them together appropriately.

Tile cutter? Already written…

Ok, so now we know that we can tell Google to get the images from somewhere else, but now we need to make the images. It’s a lot to ask from your users to take their images and cut them into 256×256 chunks. So, we will need a tile cutter, or program that slices an image into the tiles.

Luckily, a ruby tile cutter does exist, and it works perfectly with Google Maps straight out of the box. The tile cutter does not have a lot of options, but for basic tile cutting it’s perfect.

Tile server? We choose S3.

We’ve got tiles, but now we need to serve them out. Mongrel is great for Rails apps, but not so great for serving images. A lot of sites (including Obsidian Portal) use some funky rewrite rules to try to get Apache to serve the static images and such. Still, serving tiles is a fairly intense task. Every time the user moves the map or zooms in and out, it will make several image requests to the tile server. For a mongrel server, this means it’s serving images rather than handling Rails requests, which is a big no-no.

Riding to our rescue is Amazon’s S3 (Simple Storage Service). Every object stored there can be made publicly accessible, and S3 will serve up the content with the correct content-type header. That’s perfect! If we can build our URLs correctly, then S3 becomes our tile server, thereby offloading all the heavy lifting to them instead of our Mongrel server.

From what I’ve seen so far, S3 is an excellent tile server, at least in terms of speed. Tiles are served faster from S3 than Google’s servers, in my subjective experience. Further, it was even faster than Mongrel serving the tiles from localhost to localhost. Your maps will load quickly, and be very responsive.

Finally, utilizing S3 allows us to store an unlimited number of tiles, which is very important if you have several zoom levels. The ceiling is determined only by the limit on your credit card ;)

attachment_fu, bringing order to chaos

Every S3 bucket is a wild, untamed jungle, and things can easily get lost. Without directory structures or meaningful modes of organization, it can be very easy for objects to go in and never come out. Therefore, it’s very, very important to keep track of everything that is being placed in S3. For us, that means having a database record for every file in S3.

Luckily, that’s exactly how attachment_fu sees things as well. Every file managed by attachment_fu has a corresponding record somewhere, and when that record is deleted (using the destroy method), attachment_fu handles deleting the associated file. This is extremely handy for a map that may have 64, 256, or even 1024 associated tiles images.

Further, S3 communication is built right in to attachment_fu, so you, the developer, barely have to learn anything at all about interacting with S3. Just let the plugin do it.

Enough chit-chat! Show us some code!

Before you jump in and start coding, there are some steps to take first. I won’t go into detail, since it’s outside the scope of this article, but here they are:

  1. Sign up for a Google Maps API key
  2. Sign up for an Amazon S3 account
  3. Install attachment_fu
  4. Install RMagick (it’s needed by the tile cutter)

Get the modified tile cutter

The YM4R tile cutter is a command-line tool by default. Since it’s written in ruby, there’s really no reason not to make the calls directly, rather than resorting to using the shell. I slightly modified the tile cutter for this purpose, as well as to enclose it inside a module. I tried to retain its ability to be used from the shell, but I didn’t test extensively. It works for our purposes, though.

Get the modified tile cutter here: tile_image.rb Drop it into your /lib directory and everything will work like magic.

The MapImage and MapTile models

# Copyright (c) 2007 AisleTen, LLC – micah @aisleten.com
#
# Permission is hereby granted, free of charge, to any person obtaining a copy of this software
# and associated documentation files (the “Software”), to deal in the Software without
# restriction, including without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all copies or
# substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
# BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

require ‘fileutils’
require ’tile_image’
require ‘aws/s3′
class MapImage < Image
include Tiler
include AWS::S3

has_attachment :content_type => ["image/jpeg"],
:storage => :s3,
:path_prefix => “map_images”,
:size => 1.kilobytes..500.kilobytes,
:resize_to => “1024×1024!”,
:thumbnails => {:thumb_512 => “512×512!”, :thumb_256 => “256×256!”}

after_attachment_saved {|x| x.send(“create_tiles”) unless x.parent_id }

validates_as_attachment

has_many :map_tiles, :dependent => :destroy

protected

# Creates the resized images needed for tile creation.
# Then calls the tile cutter.
def create_tiles
tmpfiles = []
[:thumb_256, :thumb_512, nil].each do |t|
# Pulls down the image from S3
key = self.full_filename(t)
logger.debug(“Retrieving #{key} prior to tiling.”)
data = S3Object.value(key, @@bucket_name)

# Store to a tempfile
tmp = Tempfile.new(“tile”)
tmp.write(data)
tmp.close
tmpfiles < < tmp
end

tmp_paths = tmpfiles.collect {|f| f.path}

tp = Tiler::TileParam.new(Tiler::Point.new(0,0), 0, Tiler::Point.new(0,0), 1)
zoom = 0..2

# Tile them in a temp dir in /tmp
tile_dir = File.join("/tmp", self.id.to_s)
FileUtils.mkdir_p(tile_dir)
logger.debug("Creating tiles in #{tile_dir}")
get_tiles(tile_dir, tmp_paths, tp, zoom)
create_tile_model_objects(tile_dir)
FileUtils.remove_entry_secure(tile_dir, true)
logger.debug("Deleting temporary tile directory: #{tile_dir}")
end

# Creates a MapTile object for each tile. This allows us to track the tiles individually
# in the database to prevent them from being orphaned in S3.
def create_tile_model_objects(tile_dir)
Dir.foreach(tile_dir) do |tile_name|
if tile_name.include?('.jpg')
t = MapTile.new
t.map_image = self
t.filename = tile_name
t.content_type = "image/jpeg"
t.temp_path = File.join(tile_dir, tile_name)

logger.debug("Saving MapTile for #{tile_name}")
t.save
end
end
end
end

This is a model for a map with 3 levels of zoom. As required by the tile cutter, we create 3 different size images: 256x256 for zoom 0, 512x512 for zoom 1, and 1024x1024 for zoom 2. Since I'm lazy, I just let attachment_fu create the images by resizing the original to 1024x1024 and then using the built in thumbnail functionality to get the other sizes.

After the images are created and sized we use tile_image (via the get_tiles function) to create all the tiles in a subdirectory of /tmp. These are then used to create MapTile objects (shown below), which get uploaded back to S3.

Astute readers will note that any non-square image will get distorted by this. One solution is to pre-pad the image with extra space to make it square. That is left as an exercise to the reader ;) (Note: If someone comes up with a good way to do this with RMagick, send me the code and I'll post it and credit you.)

Astute readers will also notice that I'm wasting time and bandwidth pulling the images back from S3 after uploading them, rather than breaking in to the attachment_fu upload cycle and tiling the images before they're uploaded. I'm lazy and it works. Send the code if you know how to do it better.

Note: MapImage subclasses from Image, which is an abstract base class I use for all my images managed by attachment_fu. Single-table inheritance allows me to store multiple kinds of images in the same "images" table this way. This is not necessary to get the custom maps to work, it's just how I like to do things.

# Copyright (c) 2007 AisleTen, LLC - micah@aisleten.com
#
# Permission is hereby granted, free of charge, to any person obtaining a copy of this software
# and associated documentation files (the "Software"), to deal in the Software without
# restriction, including without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all copies or
# substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
# BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

class MapTile < ActiveRecord::Base
has_attachment :content_type => ["image/jpeg"],
:storage => :s3

belongs_to :map_image

# We overwrite the base_path method for the tiles in order to place them
# in the tiles “directory” under their parent map ID on S3
def base_path
File.join(“map_images”, self.map_image_id.to_s, “tiles”)
end
end

The MapTile class is very simple, thanks to attachment_fu. The only real trick is to overwrite the base_path method. This allows us to modify the S3 key (ie “file path”) given to the MapTile image when it is stored. Instead of being stored using its own ID, we would like to store it using the ID of its parent MapImage. This is the structure that will allow us to serve up the tiles.

The View…now with added JavaScript!

google.load(“maps”, “2″);

var map_z_offset = 2;
var map_zoom_levels = 3;

function load() {
var customType = customMap();
var map = new google.maps.Map2(document.getElementById(“map”), {mapTypes: [customType]});
var newCenter = new google.maps.LatLng(79.1, -135);
map.setCenter(newCenter, (map_z_offset));
map.addControl(new google.maps.SmallMapControl());
}

function customMap() {
var copyCollection = new google.maps.CopyrightCollection(”);
var copyright = new google.maps.Copyright(1, new google.maps.LatLngBounds(new google.maps.LatLng(-90, -180), new google.maps.LatLng(90, 180)), 0, “”);
copyCollection.addCopyright(copyright);

var high_level = map_z_offset;
var low_level = map_z_offset + map_zoom_levels – 1;
var tilelayers = [new google.maps.TileLayer(copyCollection, high_level, low_level)];
tilelayers[0].getTileUrl = CustomGetTileUrl;

var custom = new google.maps.MapType(tilelayers, new google.maps.MercatorProjection(low_level + 1), “Chart”, {errorMessage:”"});
return custom;
}

function CustomGetTileUrl(point, zoom) {
var img_id = “< %= @map_image.id %>“;
var bucket = “< %= @map_image.bucket_name %>“;
return “http://s3.amazonaws.com/” + bucket + “/map_images/” + img_id + “/tiles/tile_” + (zoom – map_z_offset) + “_” + point.x + “_” + point.y + “.jpg”;
}

google.setOnLoadCallback(load);

Warning: I will be the first to admit that I don’t know squat about JavaScript. I’m learning as fast as I can, but it probably looks terrible to someone who really knows what they’re doing.

Most of this code was lifted, almost untouched, from the Mapki page on creating custom maps (see references below). The Google Maps code specific portions of creating custom maps has been understood for a long time now. Still, I’ll explain some of the things we’re doing that are special.

I am offsetting the map by 2 zoom levels. So, when we have a tile for zoom 0, we will display it at zoom 2. This will effectively give some buffer space around the image. If you display exactly according to the zoom level, it will look a little strange. At zoom 0 (all the way out), it assumes you’re looking at the entire world (which is roughly spherical), and therefore will place the same tile multiple times in a line. This works for a world map, but looks strange for anything smaller. So, we offset a bit by fooling the system into thinking that we’re zoomed in. That way, it will pad around our image with blank space.

Near the top, you will see that I am setting the Lat/Lng to a seemingly arbitrary number. This is related to the zoom offset. Tile location 0,0 is somewhere in the arctic ocean. When the map first displays, I want zoom level 0 (a single tile) to be visible in the center. I didn’t find a quick and easy way to calculate from tile X,Y to Lat/Lng, so I just started experimenting with values. (79.1, -135) works well for zoom offset 2, while (66.66666, -90) works well for zoom offset 1. If you want to offer arbitrary zoom offsets, you’ll need to come up with a better way of handling this. Send me the code and I’ll post it.

Finally, the real meat! GetCustomTileUrl is where all the magic happens. This is how we tell Google Maps to use our custom tile server instead of the default one. The parameters are an X,Y point and a zoom. Our tile cutter names the tiles perfectly for this, and we placed them in S3 according to the ID of their parent MapImage, so all we have to do is construct the URL based on this ID.

That’s It!

Now we’ve put all the pieces together. attachment_fu handles uploading and resizing, tile_image handles cutting the tiles, and S3 handles serving them out.

Issues

Image processing on the mongrel thread

If you were paying attention, you’ve realized that when uploading a map everything happens during a single HTTP request. By my estimation, uploading to S3, downloading the resized images, running the tile cutter, and then re-uploading the images takes between 5-10 seconds for a map with 3 zoom levels. In my case, the end total is 26 images that have to be created and moved. For any reasonable size site, tying up the web server for 10 seconds is out of the question. You can counter this with more mongrels, but that’s an approach that won’t scale. Plus, the time increases exponentially with more zoom levels. Tiling 6 or 7 zoom levels could take several minutes, even on a beefy machine.

Fortunately, there’s no reason the image processing and uploading has to be done on the main thread. Simply allow the users to upload the image and push the single image to S3. In the database, flag it as “not tiled” Then, asynchronously, have a script that periodically wakes and scans the database for maps that need to be tiled. It does the tiling work and uploading and then flips the flag in the database to “tiling finished.” For the user, they upload their map and then are taken to a screen that says, “We are currently preparing your map. Please wait a few minutes for the process to complete.” In reality, if the number of maps is low, and the zoom level is small, it could only take 10-20 seconds, assuming your script wakes up frequently enough to check.

Maps must be square

Because of the way the tile cutter works (or at least how I understand it), the starting image must be perfectly square. This can be dealt with by padding the uploaded image with a neutral (or transparent, for PNG) background until it has square dimensions, then send it to the tiler. Like I said before, if someone has a good way of doing this with RMagick, send the code and I’ll post it and give credit.

No map markers

This isn’t technically an issue, it’s just another feature I want :) Next up on the feature list is the ability to add markers to your map, and drag them around. By using Google Maps as the backend, this should be quite easy to do, at least I hope so. I’ll cover that in part 2, if I ever get around to it.

Resources

These are required resources for this to work at all. If for some reason you are unable to use any one of the following, the entire approach falls apart.

  • AWS::S3 Library – The main page for the Amazon S3 Ruby library. It has great examples and documentation.
  • Amazon S3 Homepage – Go here to sign up for Amazon Web Services (AWS). Make sure to also sign up for S3, since AWS is comprised of many services, and you have to sign up individually for each one.
  • Mike Clark’s attachment_fu tutorial – Pretty much the tutorial/howto for attachment_fu
  • YM4R – Includes the tile cutter. While not strictly necessary if you use my modified tile cutter, they still deserve the credit and so are listed in the required resources.
  • Google Maps API Key signup – You will need a Google Maps API key, otherwise you can’t use the map code. It’s free, so don’t worry about it.
  • RMagick – This is a ruby interface to the ImageMagick (or GraphicsMagick) libraries. It is needed for all the image resizing and cutting.

These are some resources that I found incredibly useful when trying to figure all this stuff out. They give some good background.

Credits

Thanks to Jordan Bethea (a player in my D&D campaign) for suggesting the feature, and thanks to Scott Turnbull for guiding me to MapWoW.com and giving me tips on how to implement it.

Thanks for reading, and if you liked the article, please consider Digging it or voting for it on your favorite social bookmarking site.

Integrating Beast forum into a Ruby on Rails app – part 1

Ruby on Rails 32 Comments »

At some point, it’s almost guaranteed that you will want a forum for your website. Forums make a great way to allow your users to contact you and each other, and their persistence allows for archiving of the answers so you (hopefully) don’t have to keep answering the same questions over and over. Moreover, most users are familiar with the forum concept and interface, thereby reducing the barrier to communication.

If all you want is to get a forum up as fast as possible, there are several free/open-source/commercial solutions. They will get you up and running very quickly, provide all the functionality you need, plus tons of features you’ll probably never use. If that’s what you want, stop reading and go get one.

Assuming you’re still here, then you’re probably in the same boat as me. You’ve got a pre-existing Ruby on Rails application, and you want to add a forum. Moreover, you want to add an integrated forum that allows your users to utilize the same login credentials as they use for your current app.

Enter the Beast! Beast is a pure RoR forum application that has everything you might expect from a forum, plus offers the glimmer of hope that you’ll be able to integrate it with your current app. Still, once you dive in, the glimmer starts looking pretty dim. I approached it several times and backed off before I finally had the confidence to finish it. Luckily for others, I made a lot of mistakes and am happy to share them.

The Goal

For now, my only goal is to unify usernames and passwords, preventing the users from having to register twice. Very important to note is the fact that I am NOT talking about a single sign-on. I am comfortable (at this point) with forcing the user to sign in twice. I simply want to make sure that the username and password are the same both times. Remember: KISS and baby steps.

Before you start

Before I describe the actual approaches, I would like to give some general advice on how to proceed, regardless of your approach. These following tips could potentially save you a lot of time in the very real case that something goes horribly wrong.

Create a development branch

You are about to attempt a potentially nasty bit of integration. You might need to hack your code (and Beast’s code) in very ugly ways. You may need to compromise your whole design. Worst of all, you might fail! At the end, you may realize that it just is not worth it, and you’re destabilizing your codebase too much to justify the payoff. Wouldn’t it be nice to simply write off the time and get back to work?

From another angle, perhaps you’re in the middle of the integration when a user finds a nasty bug on your live site. You need to fix the bug right now, but your codebase is in a totally unstable state. You can’t fix the bug without checking in your Beast changes, and they will break everything. Sounds like it’s going to be a long night…

If you do your work on a development branch, then switching back to the trunk is very easy. Simply check in all your changes on the branch, then wipe out your working copy and check out a fresh one from the trunk. All the work you did on Beast is still there (in the branch), but you’re working on the trunk, totally clean. You can continue main development (such as fixing nasty bugs), and get back to Beast when time allows. Finally, when you’re satisfied that Beast is ready, a simple merge-to-trunk is all it takes to bring all your changes into the trunk.

Bottom line: Do all your Beast integration work on a branch.

Grab Beast as a vendor branch

Beast is still under active development, and they just hit 1.0 Do you plan to stay current with it as it matures over time? I definitely plan to. However, integrating it with your application will almost definitely require you to modify it in some way. Heck, just installing it by itself will probably require modifications in order to get the text to say what you want.

Starting from a vendor branch will allow you to (hopefully) easily upgrade to newer versions, while keeping all the changes you had to make in order to get it working with your app.

We have already covered how to do a vendor branch and how to upgrade using a vendor branch, so go browse those posts if you don’t know what I’m talking about.

Please, I’m begging you, trust me on this one. Do the vendor branch and save yourself a huge headache when Beast 1.1 comes out…

The Approaches

I attacked the integration problem from three different angles. Two worked and one was a total failure. I’ll describe each one and its pros and cons. If I get anything wrong, or left something out, please do not hesitate to correct me in the comments. I really want this to be a valuable resource for the Rails community.

One commonality to note between all the approaches was that I never tried to fully integrate the applications from the perspective of controllers and routing. They each retained their own directory structure, they each will have their own root domain, and they will each require their own mongrel server, unless I can figure out how to get a single mongrel to serve both.

1. Separate databases; Users pulled from my app’s database

With this approach, I tried to keep separate the two databases, except for the Users. The RDocs for ActiveRecord::Base give instructions on how to force Rails to connect to a different database for a specific model. This seemed like an excellent way to handle the Beast integration.

I added all the missing Beast user columns to my Users table, created the secondary database, and updated the Beast User model to connect to my database. At first it worked, and I was pretty excited. However, as I navigated around the forums, it crashed almost immediately. Because there are several relationships between the User and other models, the system was attempting to run some joins. Perhaps other RDBMS’s can do cross-database joins, but it seems that MySQL cannot, at least not how I have it set up.

Bottom line: Didn’t work because MySQL freaked out about the cross-database joins.

2. Same databases; Beast tables renamed with beast_

My second attempt involved adding all the Beast tables to my current database, and then renaming all the Beast tables with a “beast_” prefix. It wasn’t strictly necessary as there were no naming conflicts, but I thought it would help with readability.

I left the users table and sessions tables untouched, as I already had a users table and simply added the Beast columns to it. The sessions table seemed like something special, so I decided not to mess with it.

To accomplish the renaming, all I had to do (I thought) was edit the migrations a little, then use the set_table_name directive in each of the model classes. When this was done, things looked like they would work.

What I didn’t realize was that much of the Beast database querying is done via specifying :conditions on a find() call. I didn’t look too closely, but perhaps the Beast team found they could not be served adequately by the find_by_x_and_y finders. The end result of this was that there were several places where the table names were listed explicitly inside the query conditionals. I ran several search/replace runs for “posts”, “forums”, and so on for all the Beast tables. Of course, these strings got several hundred hits throughout the codebase, and the replacement was a real chore.

Eventually I got Beast working, but I did not have a lot of confidence in the changes. I was not sure if I had caught everything, although this could be reasonably tested using the unit and functional tests. Most troubling to me, however, was the prospect of trying to integrate a new release. I had the sneaking suspicion that every time there was a new release of Beast, I would be hunting down table names inside of conditionals and replacing them by hand. Besides the tediousness, this was bound to produce bugs that I wouldn’t catch and would escape into production.

Bottom line: Works, but you’re in for a world of hurt when you want to upgrade to a new release of Beast.

3. Same database; Beast tables unmodified

As I mentioned earlier, I had no table name conflicts with Beast, so this was a viable option for me. Just add all the new Beast tables, and modify my users table to add the necessary fields.

This was a no-brainer, and took almost no time at all to accomplish. Run the migrations, make a few modifications to the authentication system, and you’re up and running. After trying the other two methods, I was able to do this one in about 5 minutes.

This is ultimately the approach I would recommend to anyone attempting the integration. Of course, it works best if you have no table naming conflicts. However, assuming that you do, I recommend only renaming the Beast tables that actually conflict. This will minimize the amount of find/replace hunting that needs to be done, both now and for future upgrades. Remember: KISS.

Bottom line: Works, is easy to do, and represents the least amount of risk when upgrading to new releases of Beast. This is my recommended solution.

Modifications to support acts_as_authenticated

Handling the database will be the hardest part, but if you are using acts_as_authenticated, you’ll need to make a few updates to the authentication that goes on.

It actually turned out to be very easy, but remember, this does not get you single sign-on, just unified usernames and passwords.

Modify the Beast User.rb, and remove or comment out the self.authenticate method. We will be replacing this. I think there might be a way to re-alias it, but deleting works just as well.

Replace the one you removed with the following method:

def self.authenticate(login, password, activated=true)
u = find_by_login(login)
return nil unless u

crypted_pass = Digest::SHA1.hexdigest(“–#{u.salt}–#{password}–”)

return crypted_pass == u.crypted_password ? u : nil
end

As you can see, this simply finds the User by their login, and checks their password the same way that acts_as_authenticated does.

Other than that, I modified reset_login_key! to change “password_hash” to “cypted_password” To be honest, I have no idea what this does, but the two fields are basically equivalent in the two authentication schemes (I think). Anyways, it seems to work.

Next steps

So why is this titled part 1? Because I want single sign-on, of course! The eventual goal is to unify the sign in interface so that the user signs in at one place and can then navigate through the entire site, including the forums, and never have to re-authenticate.

Part 2 (if it ever gets written) will deal with unifying the sign-on. This has been done before (see the references at the end of the post), but I’m just not ready to attempt it yet.

Update 2007-12-17 Adam from the comments has posted a great tip on how to easily get single sign-on. He says:

Single sign on is very easy. ALl you have to do is set your session domain to be the same by typing (for rails 2.0)

config.action_controller.session = { :session_key => “_session_id”, :secret => “secret_session” , :session_domain => “.mydomain.com” }

So now forums.mydomain.com and www.mydomain.com will use same cookie/session and you can host beast on a subdomain as a separate app. I then added a before_filter to application.rb that ensured session[:user] was populated otherwise i redirected to www.mydomain.com/login

I have not tried this yet, but telling the two Rails apps to use the same session is probably the best way to go.

Additional References

Beast Forums – A great place to ask questions about Beast…go figure ;)
Integrating Beast with another application – Someone else who did the same thing, but duplicated the User information across 2 databases

Call for participation

If I have gotten anything wrong, or you know a better way to do it, please post a note in the comments! This integration was fairly hard to do, and I made a lot of false starts. I want to make it smoother for those that follow.

Good luck, and don’t forget to make a development branch!

subversion vendor branches in action – going from 0.7 to 0.8.5 of acts_as_solr

Ruby on Rails 13 Comments »

As we have mentioned numerous times, we here at Aisle Ten prefer vendor branches for installing our plugins. We have found that most plugins require some tweaking or modification in order to put them use exactly as we want. Rather than being a strike against the plugin architecture, I count this as one of its greatest strengths. Plugins are usually simple enough that it is an easy task to understand what they do, and then modify them to support exactly what you need.

In my previous post, I gave directions on how to create a vendor branch for a plugin. What was missing was a good explanation of how to actually upgrade when a new release version comes from the vendor. Today I’ll cover that, using the real-life example of moving from acts_as_solr 0.7 to 0.8.5. All examples will relate to acts_as_solr where the version we currently have is 0.7 and the new version we want is 0.8.5.

Update:Thanks to Chris in the comments, there may be a way to do all of this “the correct way” Skip down to the bottom to see.

What the SVN book recommends

The definitive source for info regarding subversion is the book, Version Control with Subversion. It’s available online for free. If you haven’t skimmed through it, now’s the time.

In the section on vendor branches, they give a very terse explanation of how to go about this:

To perform this upgrade, we checkout a copy of our vendor branch, and replace the code in the current directory with the new libcomplex 1.1 source code. We quite literally copy new files on top of existing files, perhaps exploding the libcomplex 1.1 release tarball atop our existing files and directories. The goal here is to make our current directory contain only the libcomplex 1.1 code, and to ensure that all that code is under version control. Oh, and we want to do this with as little version control history disturbance as possible.

After replacing the 1.0 code with 1.1 code, svn status will show files with local modifications as well as, perhaps, some unversioned or missing files. If we did what we were supposed to do, the unversioned files are only those new files introduced in the 1.1 release of libcomplex–we run svn add on those to get them under version control. The missing files are files that were in 1.0 but not in 1.1, and on those paths we run svn delete. Finally, once our current working copy contains only the libcomplex 1.1 code, we commit the changes we made to get it looking that way.

Why it doesn’t work

The main problem with this approach has to do with files and directories that are deleted between versions. At the end of the branch upgrade process, two things you want to have are:

  • current contains all and only the code from 0.8.5. In other words, current is an identical copy of 0.8.5 from the acts_as_solr repository
  • Our repository contains the history of the transition from 0.7 to 0.8.5.

Getting the second one is a little tough, and getting them both together is very tricky. From the subversion book:

We quite literally copy new files on top of existing files, perhaps exploding the libcomplex 1.1 release tarball atop our existing files and directories. The goal here is to make our current directory contain only the libcomplex 1.1 code

What about deleted files? Copying the new files in or exploding the tarball will cover changed and added files, but any files or directories that were deleted will show up in svn as unmodified Using this method as described, it is impossible to know whether a file listed as unmodified is actually present in 0.8.5 without manually verifying its existence. For acts_as_solr this would be an annoyance. For a project with thousands of files, it would be a nightmare, and the human operator would surely make mistakes, meaning what you have in your repository would not be an exact copy of the vendor’s release. That’s about the worst possible outcome.

Solution 1: Strip out all except directories and .svn

One possible solution is to “clean” your working copy before exploding the tarball. In this case, you write a script that walks your working copy deleting everything except the directories and their .svn subdirectories. Now, when you explode the tarball and get a list of changes, it will tell you all the files that are now missing (and therefore are not part of the release). Then it’s easy to follow the book’s instructions and remove them from svn.

Why it doesn’t work

Besides the fact that I have not been able to find a script that does what I’m describing, it doesn’t solve the problem of deleted directories. If, during the change in versions, entire directories have been deleted, this method will not detect that. After running the script, you’re left with an empty skeleton of your current version. Exploding the tarball will overlay the directory structure of the new version, but any deleted directories will still be present. They’ll be empty, but they’ll still be there. Again, it is impossible, without manual intervention, to determine if any particular empty directory still belongs. This is a much better situation than before, where we had to check every unmodified file, but it’s still an annoyance, and allows for human error. On to the next solution…

Solution 2: Merge against the vendor’s repository

When I thought of this one, I was very excited. It should conceivably allow me to use svn to handle all the dirty work, which is what it does best. If your vendor allows you read access to their svn repository, you should be able to do a merge (diff) between your current version, and their newest version, and apply that to the current. It’s a standard merge operation saying “What do I have to do to make my copy look like theirs?”

Why it doesn’t work

Simply enough, svn does not allow you to merge between two different repositories. Why this is, I have no idea, but they must have their reasons. So, this idea takes a bullet to the head.

Solution 3: Import, then merge against your repository

When I realized that Solution 2 would not work, it occurred to me that I could just import the newest release into my repository and then merge with that.

Step by step:

  1. Do an export from the vendor’s repository (or explode the tarball) of the latest version to somewhere local (let’s say temp_latest)
  2. Import this code into your repository: svn import temp_latest http://your/repo/here/vendor/some_package/latest
  3. Check out current to somewhere: svn co http://your/repo/here/vendor/some_package/current temp_current
  4. Merge and apply: svn merge http://your/repo/here/vendor/some_package/current http://your/repo/here/vendor/some_package/latest temp_current
  5. Commit the changes: svn ci temp_current
  6. Bonus points – verify the integrity: svn diff http://your/repo/here/vendor/some_package/current http://your/repo/here/vendor/some_package/latest If there are no differences, then current now contains an exact copy of the new release. Good job!
  7. Tag the new current: svn copy http://your/repo/here/vendor/some_package/current http://your/repo/here/vendor/some_package/2.0

At this point, you can copy the new tag to your trunk and deal with any conflicts. This is all covered in the SVN book.

Why it works…but kind of sucks

This is my preferred solution, but it’s not perfect. My main issue is that each time it’s done, you have to import a brand new copy of the code into your repository in order to do the merge. This is pretty wasteful from a storage perspective, and can be prohibitive if the import is sizeable and/or the vendor puts out new releases on a fast schedule. It won’t be a problem for (most) plugins, but for larger projects it is probably impractical.

Update: In hindsight, I don’t think this actually works the way I intended. When you import the new files, svn does not realize that they’re in any way related to the files in current. Therefore, when you do the merge, rather than applying changes to the old files, it simply deletes them and adds the new ones. In other words, the merge simply replaces all the old files with the new ones, rather than inspecting and recording the changes that occurred. This process will discard any changes that you’ve made, making the whole process fairly worthless.

Going from 0.7 to 0.8.5

We chose solution 3, import and merge, for handling the move from 0.7 to 0.8.5 of acts_as_solr. I am happy with the result, as I noticed during the process that the plugin had been seriously re-organized, resulting in the deleting and moving of many files and directories. Had we tried any of the other solutions, it would have been a painstaking process figuring out where things had moved. With the route we took, svn handled all of that.

Once I decided to go that route, the entire process took about 15 minutes. There were no conflicts to deal with when merging to trunk, but that is probably due to the fact that the only changes I made resulted in patches that were accepted back into the acts_as_solr trunk.

Think you can do better?

These are the only solutions I could come up with. Some flat-out don’t work, while others have their plusses and minuses. If you have a suggestion for a better way, please post a comment and I’ll try to include your idea into the list.

Update: svn_load_dirs

What I haven’t mentioned up until now was svn_load_dirs. This is a perl script that is supposed to help manage vendor branches. I have fumbled with it in the past and simply gotten frustrated. I just couldn’t find good documentation.

However, Chris in the comments has posted a link to an excellent post detailing svn_load_dirs, and I recommend everyone check it out. The next time I have to do a vendor branch update, I will try following these directions.

WP Theme & Icons by N.Design Studio
Entries RSS Comments RSS Log in