New SD.rb Talks Posted: Simple Sidebar Plugin & Ajax CSS Star Rating with ActsAsRateable

Plugins, Ruby on Rails No Comments »

My podcasts / vidcasts at SD Ruby have been posted to the podcast section.

Simple Sidebar Plugin

How to use Simple Sidebar plugin to DRY up sidebar content in applications.

SD.rb Vidcast - Simple Sidebar Plugin

Related Blog Posts:

http://blog.aisleten.com/2007/06/03/simplesidebar-if-you-have-sidebars-you-need-this-plugin/

Ajax CSS Star Rating with ActsAsRateable

How to build an Ajax-powered, CSS star rater using the ActsAsRateable plugin and Komodo Media’s CSS Star Rating Redux technique.

SD.rb Vidcast - Ajax CSS Star Rating

Related Blog Posts:

http://blog.aisleten.com/2007/05/03/ajax-css-star-rating-with-acts_as_rateable/
http://blog.aisleten.com/2007/05/17/find-the-top-5-highest-rated-objects-with-acts_as_rateable/

At this month’s meeting we’re going to be having our first Rails Roundtable so come and check it out.
SD.rb December Meeting Schedule

The search for credit card processing part 1 – TrustCommerce

Business, Plugins, Ruby on Rails 4 Comments »

We have finally gotten to the point where we are ready to start offering subscriptions to Obsidian Portal. We don’t expect there will be a lot of interest, but it’s always a sort of chicken v. egg problem. If you don’t have paying subscribers, then it’s not worth the effort to make the features. Conversely, without the features, no one is going to pay. On second thought, I guess it’s not chicken and egg, it’s pretty clear: you need features or no one will pay. ;)

Asking for payment means you will need to be able to accept it. Currency on the web is passed almost exclusively via credit cards (except for PayPal…), so that’s the direction we need to go in. That requires us to select a credit card processor. For today, we will be looking at TrustCommerce.

I won’t go into the details of how credit card processing works, mainly because I don’t really understand it myself. Suffice it to say, there are a lot of middle-men, and they are all trying to take a cut. Each cut is either a percentage of the total charge or a flat fee or both. So, a typical fee structure might be $0.30 flat fee plus 2.5% of the total transaction.

Note: If you don’t care about the analysis and just want to see a rundown of their prices, then jump to the pricing.

Go easy on me; it’s my first time

When selecting a processing agent, our first priority right now is ease of use. We don’t expect there will be a lot of people signing up for our premium service, so we don’t want to expend a lot of effort on a payment system only to never see it used. Also, we’re willing to pay a higher rate to the processor since 3% of $30/month is a lot different than 3% of $30,000/month. I’ll pay 3% vs 2.5% if the 3% service takes 2 hours to implement and the 2.5% service takes 10. So, for us, ease of use trumps competitive pricing.

Since we’re talking about subscriptions as opposed to purchases, there is a recurring element to the payments. Since we want easy-to-implement solutions, we are scoping our search to only include the payment processors that offer a recurring service. This is a very important thing to note, especially if you’re in the same boat. A 1-time payment processor model (like Google Checkout) just will not work if you want to do subscriptions. The main reason is that you will have to store the users’ credit card info on your server in order to pass it to the payment processor each billing cycle. Do not do this! There are actual laws and regulations detailing what sort of security procedures you have to maintain in order to hold that sort of sensitive data. It’s much easier to simply pay someone else to deal with that crap. If you do choose to store their info in your database, you should be looking for a lawyer right now, not a payment processor.

Just plug in your credit card info

In Rails, ease of use means finding a plugin. I write a lot about plugins on this blog, so why should credit card processing be any different? Doing a quick Google search led me to the TrustCommerce subscription payment plugin.

Finding this bit of code brought a smile to my face, as I thought I had just finished 90% of the work. Sign up for an account, drop in the plugin, and wait for the money to roll in. Too bad there were a few red flags that derailed the money train.

Sitting by the phone

TrustCommerce does not list any pricing on their website. Instead, they say you have to sign up for a test account, and then you’ll be contacted. Not a big deal, I guess. So, I signed up for a test account.

The first red flag went up when I did not get an immediate callback. Sure, I signed up at 11:00pm Eastern Time, but that’s normal business hours in Internet time. In other words, if you’re an Internet company that requires phone contact, you had better have someone manning the phone at all hours. A lot of Web jockeys like me have a regular 9-5 job that precludes us from doing our business dealings during normal business hours. I want to deal with companies that understand this and have staff available during my normal business hours.

Red flags: 1

The ball sits in my court

The second red flag went up at their lackluster eventual response. My cell is in a dead zone at work, so whenever I leave for lunch, I get all my messages. On the day after requesting contact, I had a voicemail message from TrustCommerce. Still no pricing info, just a short message to call them back. Seeing as how I was busy, I couldn’t do it right away. Then I forgot. Dead silence on their end. No e-mails, no more calls, nothing.

Now a lot of people may disagree with me on this, but I think they should have been hitting my inbox and voicemail pretty hard. “Mr. Wedemeyer, we’re still interested in talking to you about blah blah.” or “Send us an e-mail with the best time to call you.” That’s how the mortgage people behaved when I used LendingTree. Sure, it was annoying, but you knew they wanted your business. To me, an anemic response indicates that someone isn’t really serious about recruiting me as a customer.

Red flags: 2

Little fish: prepare to get fried

When I finally did get in touch with someone from TrustCommerce, he was quite happy to answer my pricing questions. I don’t know if I’m allowed to post that info, but since they didn’t expressly forbid it, here you go:

Basic pricing

  • $95 1-time fee
  • $20 / month
  • $0.20 / transaction

Citadel (recurring payments)

  • $145 1-time fee
  • $10 / month
  • $0.10 / month / billing id (ie. subscription)

Holy crap! $240 just to get started, plus an additional $30 per month, just to be allowed to use their service? Seeing as how I expect Obsidian Portal to be making around $10 / month, at least until we can recruit more people, this is insane! I politely said thank you to the salesman, hung up the phone, and started writing this post.

I guess I see these huge front-loaded fees like this: If you’re making enough money that the fees don’t matter, then you already have a lot of subscribers, which means you’re already handling credit cards. Maybe their service is so great compared to the competition that it’s worth it for the big boys. But, if you’re a small time operator like me, forget about it.

Red flags: 240 + 30 / month

The search continues

Although I said pricing was not our top priority, the front loaded fees with TrustCommerce completely invalidate them as a viable option. It would be a very long time before we paid off the initial investment, and with our none-to-clear business prospects with Obsidian Portal, that’s a gamble I’m not willing to take.

In the next exciting chapter we will be looking at Amazon Flexible Payment System (FPS). This new web service from Amazon is meant to rival Google Checkout and PayPal. I’ve been extremely pleased with S3, and maybe they can do one better with FPS. Stay tuned to find out.

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

Plugins, Ruby on Rails 9 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.

acts_as_fedora – A Rails plugin for the Fedora content repository

Plugins, Ruby on Rails 1 Comment »

The Background

At work I’ve been diving into the Fedora content repository system. Totally unrelated to the Linux distro of the same name, the Fedora repository is a really cool database on steroids for all sorts of heterogeneous content. Text, media, files, and metadata can all be easily associated with one another in a single package.

Unfortunately, getting the data in and out of a Fedora instance can be tricky. The interface is via SOAP calls, which while not incredibly hard, are not all that easy either. Further, the Fedora development team has a propensity for changing interfaces at an alarming rate. Being a sort of fast-and-loose developer myself, I can understand this, but it’s still a little frustrating.

When I first started playing around with Fedora, it took me about two days to finally get my Rails app to a point where I could ingest (add) and disseminate (retrieve) records from Fedora. Obviously, two days to get up and running is not something to be proud of in the Rails community. However, during the exercise, I started to get a feel for how the system worked, and the seed of a Rails plugin was planted.

Fast forward about a month, and things are looking a lot better. I have a good deal more experience as a plugin user, plus I understand the Fedora interfaces and have successfully used them quite a bit. In the spirit of giving back to all the plugin authors that have helped me immensely, I am announcing my intent to create a plugin for Fedora.

Goals

The goals for acts_as_fedora are fairly modest, at least at this stage. It might get more features in the future, but for now I’m following the KISS principle.

  1. Easy to define a Fedora digital object (ie. datastreams) in a model object.
  2. Create a new digital object by instantiating a model, filling in data values, and calling save.
  3. Retrieve a digital object from the repository by its PID.
  4. Read a datastream as an accessor on the model object (like foo.DC for dublin core).
  5. XML datastreams are preloaded into REXML::Document objects for easy processing.
  6. Update a datastream by changing its value (via an accessor) and calling save.
  7. Purge an object.

Astute observers can probably see that I’m trying to get basic CRUD functionality with respect to datastreams. We can deal with disseminators later ;)

Schedule

I have already started coding and testing for acts_as_fedora, and I hope to have a working alpha version that satisfies the stated goals by the end of June. This post is just a kick in the pants to myself in order to get moving. If you’re reading this after June 2007 and there is still nothing for acts_as_fedora, please send me a work email at mwedeme @at@ emory edu and tell me to get moving!

Use whiny_finder and RecordNotFound to turn 500 into 404

Plugins, Ruby on Rails 12 Comments »

What happens when you can’t find the record the user is looking for? Let’s say the user is looking for blog post 9999, and you’ve only got 25 posts. What should happen? Not being an expert in HTTP, I’m not really sure, but returning a 404 in this case seems like a good idea. By default, Rails is set up to translate an unhandled ActiveRecord::RecordNotFound exception into a 404 response to the client (see update below). This is just another one of the nice little hand-holding defaults that makes Rails so friendly to noobs like myself.

To support this, the default find() method is designed to throw a RecordNotFound exception any time you request a record ID that does not exist. So, find(9999) in our example will throw the RecordNotFound exception, and assuming you’re not handling it explicitly, Rails will return a 404 to the client browser. In addition, the 404.html file in your public/web directory will be served out to the client. You can put all sorts of clever hints in here to guide the user to the right spot.

Unfortunately, not all find() methods are created equal. Only the most basic find actually throws the error. This is the find where you pass in the exact ID (or a list of IDs). With other incarnations of the find method, nil is returned but no RecordNotFound is thrown. If you’re using find_by_firstname_and_lastname all over the place, you’ll never get any exceptions. If you’re good, then you’re checking to make sure the return is not nil. If you’re sloppy (like me) you don’t do any checking and tell yourself you’re too busy.

The end result is that you get a nil back and then pass it to your views. At some point, you try to access a property and you get an “undefined method XXXX for nil:NilClass” exception. Unlike the RecordNotFound, however, Rails will return a 500 for this exception. A 500 indicates an internal server error, rather than a page not found. In other words, you’re admitting to the client, “I write sloppy code and something went wrong.”

One solution to the problem is to check every time you call find. If nil is returned, then raise a RecordNotFound exception. This will translate to a 404 and everyone is happy. “But I’m lazy,” you complain. “Bounds checking is just so boring.” Lucky for you (and me) someone else is just as lazy, but a lot more clever. They had the same idea and decided to code this behavior as a plugin, thus allowing the rest of us to DRY out our controllers.

The whiny_finder plugin is a simple and elegant bit of code that adds a “find!” (bang) complement to all your model classes. Following the standard Ruby convention, using the bang version of the method behaves exactly as the original, except when nothing is found a RecordNotFound exception is thrown. Where before we only had find_by_firstname_and_lastname, we now also have find_by_firstname_and_lastname! That last exclamation point makes all the difference.

To get the whiny_finder plugin, just run the standard plugin installer:

script/install http://soen.ca/svn/projects/rails/plugins/whiny_finder/

At that point, just take a couple minutes and do a search for “find_by_” and “find(:first” In a jiffy you can replace all your regular silent finders with their whiny counterparts. Then you’re set!

By installing a single plugin and spending 10 minutes updating your finders, you can turn tons of 500 “server error” into 404 “page not found” Your users (and friendly robots) will be better informed as to what’s going on, and your sloppy programming skills will once again be safely hidden away. What do you have to lose?

Update: Thanks to Tony in the comments, I have been informed that Rails 1.2.3 is not set up to respond with 404 on an unhandled RecordNotFound exception. This seems to have been added in Edge Rails, however.

It was working for me because I’m using the most up-to-date version of the exception_notification plugin, which seems to have been updated to match Edge Rails. I’m still using 1.2.3 for Rails, but getting exception_notification gave me the updated exception handling.

So, to get the 404s from unhandled RecordNotFound exceptions, you need to do one of the following:

  • Get Edge Rails
  • Get exception_notification (** what I would recommend **)
  • Override rescue_action_in_public in your application.rb controller.

SimpleSidebar – If you have sidebars, you need this plugin.

Plugins, Ruby on Rails 12 Comments »

Why you need it

Like many other websites out there, Obsidian Portal arranges its content into multiple columns. We have the main content in the center column, and ancillary content in the right sidebar. It’s a fairly standard organization scheme and it works well for us.

Normally, this sort of organization is handled in Rails via the use of layouts. One layout may contain the sidebars for your homepage, another one holds the sidebars for the user’s profile page, and so on. In the beginning, this works.

However, once your application reaches a certain level of complexity, this approach begins to show its true inflexibility. In many cases, you will have some sidebars that are common to every page, such as a login or search bar. You can split these into a partial, but you still have to include that partial in every layout. Not a big deal, but frustrating. Soon after this, you’ll run into cases where you need different sidebars based on the particular action for a controller. Perhaps there are some standard info sidebars for showing and listing, but for editing and creating you want a sidebar that has some editing tips. Now, if you’ve been using the convention for layouts, you probably have a layout for each controller that is named the same as the controller. That way, Rails will automatically select that layout. To support the different sidebars for different actions, you’ll have to clutter that layout up complicated if/then conditionals. Very quickly the template contains more Ruby than HTML.

Trust me, we went this route initially, and it became a nightmare. Here is one of our layouts, prior to SimpleSidebar:

< %= render :partial => “layouts/header” %>

< %= render :partial => ‘account/login’ -%>

< % if controller.action_name == 'show' %>

Description< % if session[:user] && permit?("game_master of :campaign") %> (< %= link_to 'Edit', {:action => ‘edit’, :id => @campaign._id} %>)< % end %>
< %=h @campaign.description %>

Party< % if session[:user] && permit?("game_master of :campaign") %> (< %= link_to 'Edit', {:action => ‘edit_members’, :id => @campaign._id} %>)< % end %>

GM: < %= link_to @campaign.game_master.login.capitalize, :controller => “account”, :action => “profile”, :login => @campaign.game_master.login %>

    < % players_with_pcs = [] %>
    < % for pc in @campaign.player_characters %>

  • < %=h pc.author.login %> (
    < %= link_to h(pc.name), :controller => ‘game_contents’, :action => ‘show’, :id => pc._id %>
    )
    < % players_with_pcs << pc.author %>
  • < % end %>
    < % for player in @campaign.players %>
    < % if !players_with_pcs.include?(player) %>

  • < %=h player.login %>
  • < % end %>
    < % end %>


< % end %>

< % if (controller.action_name == 'list') or (controller.action_name == 'search') %>
< %= render(:partial => ‘search/search_bar’, :locals => {:search_type => ‘Campaigns’, :search_controller => ‘campaigns’, :search_action => ‘search’}) %>

< % end %>

< % if flash[:error] %>
< %= flash[:error] %>

< % end %>
< % if flash[:warning] %>

< %= flash[:warning] %>

< % end %>
< % if flash[:notice] %>

< %= flash[:notice] %>

< % end %>
< %= yield %>

< %= render :partial => “layouts/footer” %>

Ignoring the fact that I’m terrible at writing templates, you can see there’s a fair amount of if/then conditional code sprinkled in. The Login sidebar is supposed to be there always, the Description and Party sidebars are only supposed to appear in the show action, and the Search bar is only supposed to be there in the list or search action. Ugh! And, this is only one of several layouts for several controllers. Each one had this sort of conditional garbage all over the place.

With SimpleSidebar, much of this conditional code moves into your controllers. Much like before and after filters, you define sidebars for a controller (or a particular action) and can give conditions on when they appear.

For example, using the previous example, with SimpleSidebar, I added the following to my controller:

sidebar :login, :unless => :logged_in?
sidebar :welcome, :if => :logged_in?

sidebar :campaign_description, :o nly => :show
sidebar :campaign_party_info, :o nly => :show
sidebar :campaign_search, :o nly => [:list, :search]

This allowed me to reduce my layout to the following:

< %= render :partial => “layouts/header” %>

< %= render_sidebars %>

< % if flash[:error] %>
< %= flash[:error] %>

< % end %>
< % if flash[:warning] %>

< %= flash[:warning] %>

< % end %>
< % if flash[:notice] %>

< %= flash[:notice] %>

< % end %>
< %= yield %>

< %= render :partial => “layouts/footer” %>

A little nicer, huh? If you look closely, there is nothing in there specific to any particular controller or action. Once I converted all my controllers over to SimpleSidebar, I noticed that all my layouts were identical, meaning I was able to delete most of them and just keep a single standard layout.

How to use it

SimpleSidebar is incredibly easy. I was up and running in about ten minutes (including the vendor drop). I was finished refactoring all my ugly templates in about 1.5 hours. Not bad for a major refactor.

Note: These instructions are for revision 13 of the plugin. Beware if you’re using a different version (although it probably won’t change that much).

1. Get the plugin

As always, it’s very easy to install plugins. Just fire up your plugin installer

script/plugin install svn://rubyforge.org/var/svn/simplesidebar

Likewise, as always, we here at AisleTen suggest you skip the plugin installer and do it manually using vendor drops. Just be sure to execute the install.rb when you’re done.

2. Update your controllers

This is where most of the magic of SimpleSidebar occurs. Rather than dealing with sidebars and their conditional display in the view layer, SimpleSidebar moves it to the controller layer. For example, your controller might look like this:

def MyController < ApplicationController
sidebar :login, :unless => :logged_in?
sidebar :welcome, :if => :logged_in?
sidebar :search
sidebar :edit_help, :o nly => [:edit, :new]


end

If you’re familiar with filters, then this probably looks very familiar. When an action is executed in this controller, the following will be run:

  1. If the logged_in? method returns false, then the login sidebar is appended to the list of sidebars to display.
  2. If the logged_in? method returns true, then the welcome sidebar is appended to the list of sidebars to display.
  3. The search sidebar is appended to the list of sidebars to display.
  4. If the current action is edit or new, then the edit_help sidebar is appended to the list of sidebars to display.

3. Add the partials.

If you’ve been good, you already have all your sidebars as partials. If not, it’s time to refactor as such. The partials need to be named the same as the sidebars listed in your controllers. Using our previous example, we will need four partials:

  • _login.rhtml
  • _welcome.rhtml
  • _search.rhtml
  • _edit_help.rhtml

These partials must be placed in the app/views/sidebars directory.

4. Update your layout.

Inside your layout, the code becomes very simple. All you have to do is add <%= render_sidebars %> wherever you want the sidebars displayed.

I highly recommend extracting this to your layouts. That way, rather than calling render_sidebars in each template you render, you just put it into the layout and know it gets called every time the layout is rendered around your template.

5. Delete all the layouts no longer needed.

Assuming you’re like me, you’ll quickly find that many of your layouts are no longer needed. Feel free to consolidate them as appropriate.

Advanced Features

SimpleSidebar also contains some advanced features that I have yet to experiment with, but I’ll still give a quick overview.

Components

If I understand it correctly, components are sidebars that also have an associated controller. This is a very exciting feature, as it could allow you to execute multiple controllers for a single request.

A good example of where this is appropriate is for dynamic sidebars, such as the “Recently Updated Campaigns” sidebar you see on the Obsidian Portal homepage. I need to execute a database query (find) in order to get this. Currently, I’m just doing it inside the partial, which is not really what you’re supposed to do. When I get a chance, I’ll see if I can refactor this and use a component to move the database querying into a controller and make the partial a dumb template.

Sidebar ordering and sorting

In the previous examples, sidebars were ordered simply by their place in the controller. However, if you need more advanced functionality, SimpleSidebar supports ordering and sorting.

Moving sidebars is accomplished like this:

sidebar_move :login, :top
sidebar_move :search, :bottom
sidebar_move :search, :up
sidebar_move :login, :down

Easy enough…

Sorting looks a little more complicated. If I figure it out (or ever have a need for it), then I’ll update how it’s done. Until then, just look in the plugin source for sidebar_sort.

Issues

The only issue I can raise against SimpleSidebar (and it’s a long shot) is that by design it mixes controller and view functionality. I think sidebars are an aspect of the view, but with SimpleSidebar you deal with them in the controller.

In all seriousness, though, this is like whining over the color of the inside of the glove box in a luxury car. In no way does it take away from the sheer awesomeness of the item in question.

Final thoughts

After my brief introduction, I am completely in love with SimpleSidebar. It has quickly made its way into my bag of permanent plugins for all Rails projects. In any future project, it will be the one I install right after acts_as_authenticated. Yes, it’s that awesome.

Find the top 5 highest rated objects with acts_as_rateable

Plugins, Ruby on Rails 2 Comments »

So you’ve got the Ajax CSS Star Rating System with acts_as_rateable (A MUST READ). Now you want to list the top rated objects and how many votes they’ve received. Fortunately Ruby on Rails and ActiveRecord makes this fairly easy by using an object’s find method.

Code

@TopRatedCampaigns = Campaign.find(:all,
:select => “campaigns.*, avg(ratings.rating) AS rating, count(ratings.rating) AS votes”,
:joins => “LEFT JOIN ratings ON campaigns.id = ratings.rateable_id”,
:conditions => “ratings.rateable_type = ‘Campaign’”,
:group => “ratings.rateable_id”,
:o rder => “rating DESC, votes DESC”,
:limit => 5)

Next in your view you will simply want to loop through each item and display it as a list:

< % unless @TopRatedCampaigns.nil? -%>
Top Rated Campaigns

    < % @TopRatedCampaigns.each do |x| -%>

  1. < %= link_to h(x.name), campaign_url(:id => x._id) -%> (< %= number_with_precision(x.rating, 1) -%>)
  2. < % end -%>

< % end -%>

Note: the campaign_url is defined in the config\routes.rb. This is optional, but I like using named routes in case you ever decide to change from using Rails standard routing. It’s also less code and a little cleaner than listing out the controller, action, and id.

map.campaign ‘campaign/:id’, :controller => ‘campaigns’, :action => ‘show’

Results

Based on today’s stats here are the top rated campaigns from Obsidian Portal:

Top Rated Campaigns

  1. Kensing (5.0)
  2. Arkanopolis (5.0)
  3. Hoarding Knowledge (5.0)
  4. Marvel 1984 (4.3)
  5. Beeropia (3.0)

Issues

The issue with this current example is if someone sets up a new campaign and rates it a 5 it will be at the top of the list even though a campaign with a 4.8 rating and 20 votes should probably be at the top of the list.

One solution would only allow campaigns with more that 5 votes to be displayed. This would be accomplished by changing the conditions in the find method. Another solution would be to somehow normalize the ratings per vote. I have not implemented either of these yet, but any feedback would be greatly appreciated.

References

Props to Patrick Crowley at moko.labs for his help with the setting up the find method.

Demystifying resource_feeder for RSS feeds – polymorphic URLs

Plugins, Ruby on Rails 5 Comments »

If you’re here, then you’re trying to figure out how to serve out an RSS or Atom feed from your Ruby on Rails application. You’ve probably already read a post by Ryan Daigle covering the basics of using resource_feeder to do exactly what you want. After all, Ryan’s post is the #1 hit on Google for resource_feeder.

Unfortunately, there are a few extremely important points that are curiously absent from the posting regarding polymorphic URLs. For those that don’t know, polymorphic URLs are…well I don’t know either. All I know is that it has something to do with the routes.rb file and named routes. For my rails project, I luckily (by accident) had one set up for the first RSS feed I tried to create. For the second, no such polymorphic URL existed, causing it all to come crashing down.

The problem is due to the fact that each item in the feed is expected to have a URL. If you’re lucky enough to have set up your routes correctly, the URL will be automagically determined. If not, then you get a cryptic error about polymorphic URLs. There may be a way to create a URL to each item in the feed, but resource_feeder simply cannot figure it out automagically.

Luckily, resource_feeder was designed to allow you to skip polymorphic URLs until you can figure out what the heck they are and how they work. If that’s the boat you’re in, then read on and I’ll explain.

1. Read the howto on Ryan’s Scraps

Ryan does a great job of explaining the main concept of resource_feeder. I even borrowed his example model object, Post. What I cover here are just the frustratingly absent portions that bugged me.

2. Get the plugins

script/plugin install http://dev.rubyonrails.org/svn/rails/plugins/simply_helpful/
script/plugin install http://dev.rubyonrails.org/svn/rails/plugins/resource_feeder/

Of course, we here at Midnight Oil always prefer explicit vendor branches over the basic plugin installation. Spend 10 minutes today and save countless tomorrow.

3. Add the action to your controller

Using the standard blog post example for this howto, here’s how you would add an RSS feed for your blog postings:

class PostsController < ApplicationController
def rss
render_rss_feed_for Post.find(:all, :o rder => ‘created_at DESC’, :limit => 10)
end
end

However, this assumes that you’ve got a title and description field in your model, and you also have a polymorphic URL for Post objects in your routes.rb file. If you’re like me, you might have one or two of these, but not all, and especially not the route. It’s something on my list to add tomorrow ;)

3.5 Add the real action to your controller

class PostsController < ApplicationController
def rss
render_rss_feed_for Post.find(:all, :o rder => ‘created_at DESC’, :limit => 10), {
:feed => {
:title => ‘Recent Posts’,
:link => url_for(:controller => ‘posts’, :action => ‘list’, :o nly_path => false),
:pub_date => :updated_at
},
:item => {
:title => :name,
:description => :tagline,
:link => Proc.new {|post| url_for(:controller => ‘post’, :action => ‘show’, :id => post.id)}
}
}
end
end

Note that it’s not too different from the original example, but it has fleshed out a few key points. The true magic happens in the :link part of the :item hash. resource_feeder will accept a Proc object as the value for :link. This Proc object is passed the “resource” in question when it is called. The resource passed is the model object being rendered as the item for the RSS feed, in other words, the items in the list passed as the first argument to render_rss_feed_for. In our example, each “resource” is a Post object. With our Post object in hand, it’s quite easy to create a URL using url_for.

As to the other items, this example assumes that you have :name instead of :title and :tagline instead of :description for your Post objects.

4. Have fun!

At this point, you should be able to create your RSS feed without worrying about polymorphic URLs or any such business. They’re probably super-easy to create, much easier than what I’ve described, but I just don’t know.

Also, this should all be applicable to the Atom feeds as well. I just used RSS as my example, since Ryan already covered the overlap between RSS and Atom as far as resource_feeder is concerned.

Ajax CSS Star Rating with acts_as_rateable

Plugins, Ruby on Rails 81 Comments »

CSS Star Rating Systems are all the rage now. Komodo Media brought this idea to the web’s attention with their post CSS Star Rating Redux. This tutorial will show you how to bring this rating system to Ruby on Rails

Example

4.0/5 Stars (2 votes)

  • Currently 3.0/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5

Several other websites have taken similar approaches:

So how do I get this to work with Rails?

I tried implementing David Naffis’ post “Ruby on Rails, Ajax & CSS Star Rating System”; however, I ran into several issues.

  1. Limited to one rating per page
  2. Limted to one type of object
  3. RJS didn’t seem to work
  4. Didn’t work with objects that were subclassed

Lets get started!

Note: where ever you see the text “Myobject“, you will want to replace this with the class name of the object that you are rating.

1. Install the acts_as_rateable plugin

piston import http://juixe.com/svn/acts_as_rateable/ vendor/plugins/acts_as_rateable

Note: We use vendor branches for plugins, not script/plugin install so that we can make modifications to the plugin (if needed) and easily update the plugin to the latest revision while keeping the patches that we have created.

2. Create the Migration

script/generate migration add_acts_as_rateable_plugin

file:db\migrate\xxx_add_acts_as_rating_plugin.rb

class ActsAsRateable < ActiveRecord::Migration
def self.up
create_table “ratings”, :force => true do |t|
t.column “rating”, :integer, :default => 0
t.column “created_at”, :datetime, :null => false
t.column “rateable_type”, :string, :limit => 15, :default => “”, :null => false
t.column “rateable_id”, :integer, :default => 0, :null => false
t.column “user_id”, :integer, :default => 0, :null => false
end

add_index “ratings”, ["user_id"], :name => “fk_ratings_user”
end

def self.down
drop_table :ratings
end
end

3. Update your existing Model

You will need to include the acts_as_rateable at the top of your class file.

class Myobject < ActiveRecord::Base
acts_as_rateable

end

Note: if you are using this with models that are subclassed you will want to put the acts_as_rateable in the parent class only.

4. Create the Ratings Controller

David Naffis’ post Ruby on Rails, Ajax & CSS Star Rating System started me off in the right direction; however, the ratings were not working for classes that were subclassed. Notice the function def get_class_by_name. This function allows the rating controller to find the base class and set the rating at the base class level.

Also note the before_filter :login_required. This verifies that the user is logged in before it runs the rate function. The function login_required is part of the acts_as_authenticated plugin and must be patched in order to work properly with the Ajax request. The patch for acts_as_authenticated is explained in step 8.

script/generate controller ratings rate

file: app\controller\ratings_controller.rb

class RatingsController < ApplicationController
before_filter :get_class_by_name
before_filter :login_required

def rate
return unless logged_in?

rateable = @rateable_class.find(params[:id])

# Delete the old ratings for current user
Rating.delete_all(["rateable_type = ? AND rateable_id = ? AND user_id = ?", @rateable_class.base_class.to_s, params[:id], @current_user.id])
rateable.add_rating Rating.new(:rating => params[:rating], :user_id => @current_user.id)

render :update do |page|
page.replace_html “star-ratings-block-#{rateable.id}”, :partial => “rate”, :locals => { :asset => rateable }
page.visual_effect :highlight, “star-ratings-block-#{rateable.id}”
end

end

protected

# Gets the rateable class based on the params[:rateable_type]
def get_class_by_name
bad_class = false
begin
@rateable_class = Module.const_get(params[:rateable_type])
rescue NameError
# The user is messing with the content_class…
bad_class = true
end

# This means the user is doing something funky…naughty naughty…
if bad_class
redirect_to home_url
return false
end

true
end

end

5. Create the Rate Partial

Again David Naffis’ post Ruby on Rails, Ajax & CSS Star Rating System started my off in the right direction and Igvita’s post Rails 4-State Ajax & CSS Star Rating helped demonstrate out to keep this partial DRY. As usual, I have expanded on both.

file: app\views\rating\_rate.rhtml

< % @convert = %w[one two three four five] %>

Notice: the div is unique ids based on the object’s id (ex: star-ratings-block-1, star-ratings-block-2, …) so that you can include the partial multiple times on a page for multiple objects (currently must be of the same class). This works specifically for the “list” view.

6. Including the Partial

Now that you’ve created the partial, you can simply include it in the view of the object that you will be rating.

< %= render :partial => “rating/rate”, :locals => { :asset => myobject } %>

7. Make it pretty with CSS

At this point you will have a list of numbers that you can click on to set the rating. Using the star_rating.gif image and css from Komodo Media

Star Rating

.star-rating,
.star-rating a:hover,
.star-rating a:active,
.star-rating a:focus,
.star-rating .current-rating{
background: url(‘/images/star_rating.gif’) left -1000px repeat-x;
}
.star-rating{
position: relative;
width: 150px;
height: 30px;
overflow: hidden;
list-style: none;
margin: 0;
padding: 0;
background-position: top left;
}
.star-rating li{
display: inline;
}
.star-rating a,
.star-rating .current-rating{
position: absolute;
top: 0;
left: 0;
text-indent: -1000em;
height: 30px;
line-height: 30px;
outline: none;
overflow: hidden;
border: none;
}
.star-rating a:hover,
.star-rating a:active,
.star-rating a:focus{
background-position: center left;
}
.star-rating a.one-stars{
width: 20%;
z-index: 6;
}
.star-rating a.two-stars{
width: 40%;
z-index: 5;
}
.star-rating a.three-stars{
width: 60%;
z-index: 4;
}
.star-rating a.four-stars{
width: 80%;
z-index: 3;
}
.star-rating a.five-stars{
width: 100%;
z-index: 2;
}
.star-rating .current-rating{
z-index: 1;
background-position: bottom left;
}
.star-ratings-block {
font-size: .75em;
text-align: center;
width: 100%;
}

8. Patch for acts_as_authenticated

As mentioned in Step 4 the acts_as_authenticated plugin must be patched to work with Ajax requests.

If you have already installed the acts_as_authenticated plugin you will need to patch it in 2 locations because the acts_as_authenticated plugin is a code generating plugin.

8A. First we’ll patch the plugin itself

The following code is inserted at line 70 into the access_denied method

file: vendor\plugins\acts_as_authenticated\generators\authenticated\templates\authenticated_system.rb


accepts.js do
render(:update) { |page| page.redirect_to(:controller => ‘/account’, :action => ‘login’) }
end

I have made a patch available for download: acts_as_authenticated Ajax Patch

8B. Second we’ll patch the lib file that the plugin generated

This is were we add the same patch to the existing lib file that was created by the acts_as_authenticated plugin if you have already installed it into your rails app. This is the exact same code as shown in 8A. In this case I will show you the whole code for the access_denied method.

file: lib\authenticated_system.rb

def access_denied
respond_to do |accepts|
accepts.html do
store_location
redirect_to :controller => ‘/account’, :action => ‘login’
end
accepts.js do
render(:update) { |page| page.redirect_to(:controller => ‘/account’, :action => ‘login’) }
end
accepts.xml do
headers["Status"] = “Unauthorized”
headers["WWW-Authenticate"] = %(Basic realm=”Web Password”)
render :text => “Could’t authenticate you”, :status => ’401 Unauthorized’
end
end
false
end

9. Test Cases

Finally it wouldn’t be a rails app if you didn’t include the test. Below you’ll find two test to confirm that the rating system works and that the user must be logged in to rate an object.

# Test campaign rate
def test_campaign_rate
@request.env['HTTP_REFERER'] = ‘/myobject/list’
login_as(:jdoe)
post :rate, {:id => 1, :rateable_type => “Myobject”, :rating => 5 }
assert_equal 5, Myobject.find(1).rating
end

# Test campaign rate – make sure you must be logged in
def test_campaign_rate_unsuccessful_if_not_logged_in
@request.env['HTTP_REFERER'] = ‘/myobject/list’
post :rate, {:id => 1, :rateable_type => “Myobject”, :rating => 5 }
assert_equal 0, Myobject.find(1).rating
end

Out in the wild

Here’s where you can go check it out on the website Obsidian Portal http://www.obsidianportal.com/campaigns/list.

Note: As mentioned above the rating table uses a user_id to make sure that there is only one rating per user per object.

References

http://komodomedia.com/blog/index.php/2006/01/09/css-star-rating-part-deux/

http://komodomedia.com/blog/index.php/2007/01/20/css-star-rating-redux/

http://swik.net/Rails/Dave+Naffis+-+Rails,+Ruby,+Randomness/Ruby+on+Rails,+Ajax+&+CSS+Star+Rating+System/kqn0

http://www.igvita.com/blog/2006/09/02/4-state-rails-ajax-css-rating/

Alternatives Ideas

http://www.robarov.be/rate/

http://www.robarov.be/rate2/

Update 2007-11-30: All code corrections from the comments have been integrated into this article..

acts_as_solr for development and production in one Tomcat instance

Plugins, Ruby on Rails 5 Comments »

In my previous post on acts_as_solr, I noted that as of 0.7, acts_as_solr did not support placing the development and production Solr instances inside the same servlet containers. This is because it differentiates between the two based on the host and port, and a single servlet container is always going to be at a single host/port.

Luckily, the author did a great job of architecting the plugin, and there is only one line where it references the servlet path. So, to configure it to talk to a different servlet in the same container takes only a single change to a single line :)

Without further ado, here’s how to do it.

1. Get the patch (see update below)

First, download and apply the patch I made to acts_as_solr. This patch is for version 0.7, and it makes the changes I described above. You can get the patch here.

All the patch does is add another configuration option to the solr.yml file, then use this option when constructing the URL for the request to Solr.

Note: This patch is distinct from my earlier patch regarding acts_as_solr and single table inheritance. Feel free to apply either one or both, as per your needs.

Update: acts_as_solr was recently updated to 0.8, and this patch was included as one of the changes. So, if you’re using 0.8 or higher, it is unnecessary. The single table inheritance patch was included in 0.8 as well, so both are unnecessary.

2. Install Tomcat

Although the patch should work for any servlet container, the remainder of this HOWTO will deal with Tomcat 5.5 since that is the only servlet container I am really familiar with. For reference, I am using Tomcat 5.5.23, so if all else fails, grab that exact release and see if things work.

I will assume that you already have Tomcat installed and can get to the Tomcat welcome page at http://localhost:8080/. If not, there are plenty of other tutorials explaining these initial stages. Try this one I found with a quick Google search. Come back here once you can see the kittie-cat :)

3. Create the dual solr home directories

Each instance of Solr is going to need its own solr home directory. This directory will contain the configuration options as well as the index data itself. In our case, the configurations will be identical, but we want to maintain distinct indices.

Make two copies the solr directory that came as part of the Solr distribution:

cp -r apache_solr_directory/example/solr /some_path/solr_dev
cp -r apache_solr_directory/example/solr /some_path/solr_prod

The base path for these directories is up to you. Put them somewhere you’ll remember.

You will also need to copy the schema.xml file from acts_as_solr into the two directories as follows:

cp myrailsapp/vendor/plugins/acts_as_solr/schema.xml /some_path/solr_dev/conf
cp myrailsapp/vendor/plugins/acts_as_solr/schema.xml /some_path/solr_prod/conf

4. Create the dual solr webapps

Each instance of Solr is going to need its own web application archive (WAR) in the Tomcat webapps directory. We will name them in such a way as to make their servlet paths distinct.

Move two copies of the Solr war file to the Tomcat webapps directory:

cp apache_solr_directory/dist/apache-solr-XXXX.war /my/tomcat/home/webapps/solr_dev.war
cp apache_solr_directory/dist/apache-solr-XXXX.war /my/tomcat/home/webapps/solr_prod.war

At this point, we have two copies of the Solr war in the webapps directory, each with a distinct name. This will allow Tomcat to create two separate instances, each with its own path.

Note: At this point, you will not be able to get to the servlets if you start up Tomcat, so don’t bother testing yet. Do the next step first.

5. Create the Tomcat context files

Each Solr instance needs to know how to find its home directory. The Solr wiki describes this in general, but I’ll try to be explicit.

Place the following two context files in the /my/tomcat/home/conf/Catalina/localhost/ directory:

solr_dev.xml

<Context path="solr_dev" docBase="solr_dev.war" debug="0" crossContext="true">
  <Environment name="solr/home" type="java.lang.String"
    value="/some_path/solr_dev" override="false" />
</Context>

solr_prod.xml

<Context path="solr_prod" docBase="solr_prod.war" debug="0" crossContext="true">
  <Environment name="solr/home" type="java.lang.String"
    value="/some_path/solr_prod" override="false" />
</Context>

Just make sure that the two value attributes point to the two copies of the solr directory that you made in step 3.

If you restart Tomcat, you should be able to test your dual installations. There should be a Solr installation at two addresses now: http://localhost:8080/solr_dev/admin and http://localhost:8080/solr_prod/admin.

6. Update the solr.yml config file

If you look closely at the patch, you can see the new servlet_path option that needs to be set. I left the setting to solr for backward compatibility, but we’re going to need to change that.

Here is an example of a solr.yml config file that works with Tomcat/Solr as we have currently set it up.

solr.yml

# Config file for the acts_as_solr plugin.

development:
  host: localhost
  port: 8080
  servlet_path: solr_dev

production:
  host: localhost
  port: 8080
  servlet_path: solr_prod

7. Finished

That’s it! At this point, you have two separate instances of Solr running inside a single Tomcat container. Your development and production indices will remain distinct, and you don’t have to worry about running multiple servlet containers on multiple ports.

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