Joey bookmarked Top 10 Performance Problems
Joey bookmarked Tynt, the Copy/Paste Jerks
Joey bookmarked Haml / Sass 3 Released
Joey bookmarked Accent Folding for Auto-Complete
A great callout about user experience in an app that works with multiple languages. The author correctly identifies the problem of languages like Japanese that use multiple writing systems, but I would add Chinese to that list as well with its traditional and simplified character sets, which don't all nicely map one-to-one. Chinese is massively popular on the internet, poised to surpass English in fact, and a lot of that content (from HKers, Taiwanese, Singaporeans and overseas Chinese) uses the traditional system.
Joey bookmarked Live Coding Rails 3 Upgrade
Joey bookmarked 2010: The Year of the Products + A New Way of Working
In any Rails app with a user system, certain actions are likely to have a before_filter to ensure that a user is either logged in or not logged in before accessing those actions. For instance, the edit profile page should only be accessible if a user is logged in. The registration page, conversely, should only be accessible if a user is NOT logged in.
With Shoulda, we can easily create a few macros which make testing this behavior as simple as adding a single line to the test context for each action that uses one of these filters.
# test_helper.rb
# ...
class ActiveSupport::TestCase
include RR::Adapters::TestUnit
def self.should_require_user(opts = {})
simulate_no_user = lambda do
stub(@controller).current_user { nil }
@preserve_login = true
end
should "require user", :before => simulate_no_user do
assert_contains flash.values, (opts[:flash] || /Please log in/)
assert_redirected_to login_path
end
end
def self.should_require_no_user(opts = {})
simulate_user = lambda do
stub(@controller).current_user { opts[:user] || Factory(:user) }
@preserve_login = true
end
should "require no user", :before => simulate_user do
assert_contains flash.values, (opts[:flash] || /Please log out/)
assert_redirected_to root_path
end
end
# ...
end
def login(user)
stub(@controller).current_user { user } unless @preserve_login
end
By adding one of these macros to a Shoulda context it will, before that context's setup block, simulate a user being logged in (or out), execute the setup block (preserving the state of @controller.current_user), and then ensure that the appropriate redirect occurs and flash message is set.
So, with these macros defined, our users_controller test looks like this:
# users_controller_test.rb
require 'test_helper'
class UsersControllerTest < ActionController::TestCase
context 'get new' do
setup { get :new }
should_require_no_user
# ...
end
context 'get edit' do
setup do
login Factory(:user)
get :edit
end
should_require_user
# ...
end
# ...
end
The resulting tests are succinct, explicit and easy-to-type.
You can recycle this pattern for other before_filters in your app too. For instance, if you have admin-only controllers / actions, just add macros that simulate an admin instead of a regular user and you're ready to test.
Joey bookmarked SelectorGadget: point and click CSS selectors
Thinking Sphinx is a great search library for Ruby projects. It has clean syntax and some nice options for indexing, searching and sorting. In this tutorial I'll show how you can use TS to perform searches based on both a geographical point and a keyword.
Let's say that we're building a Rails app to index independent coffee shops in our town. Every coffee shop has a name, a description and some comments, as well as a latitude and longitude value so that we can place it on a map. We want to allow our users to search through the coffee shops in our database by providing keywords and a location. We want our searches to take into account BOTH the relevance of the results based on keyword matches, as well as their proximity to the location given.
First, we'll of course need to install Sphinx and Thinking Sphinx. It's probably also a good idea to read this introduction to Sphinx.
This tutorial also uses the Geokit gem, so be sure to grab that as well.
Once Sphinx and Thinking Sphinx are installed, we are ready to define the indexes on our model. This tells Sphinx which fields to store in its index for searching, which attributes we want to have available for sorting and filtering, as well as any other properties we want to define.
# Table name: coffee_shops
#
# id :integer(4) not null, primary key
# name :string(255)
# description :text
# lat :float
# lng :float
class CoffeeShop < ActiveRecord::Base
has_many :comments
PER_PAGE = 10
define_index do
# fields
indexes :name
indexes :description
indexes comments.body, :as => :comments
# attributes
has 'RADIANS(lat)', :as => :lat, :type => :float
has 'RADIANS(lng)', :as => :lng, :type => :float
# properties
set_property :latitude_attr => 'lat'
set_property :longitude_attr => 'lng'
set_property :field_weights => { 'name' => 10,
'description' => 2,
'comments' => 1 }
end
end
The fields in the index tell Sphinx that we want our search to look at the name and description of our coffee shops, as well as the body of any comments that have been made. Notice that we're able to index not only fields that are in our coffee_shops table, but also fields from associated records - in this case the bodies of our comments.
Defining a lat and lng attribute are necessary for doing geography-based searches. The big gotcha here is that Sphinx needs these attributes to be stored as radians, whereas most geocoding APIs (such as Google) use decimal degrees. The SQL 'RADIANS(lat)' will automatically do this conversion for you. If you happen to have your lat and lng stored as radians already, however, you can just define your attributes like this:
# attributes
has :lat
has :lng
Finally, we define our properties. The :latitude_attr and :longitude_attr properties tell Sphinx which fields we're using for our geography calculations. The :field_weights define how much weight we want to give to each indexed field. If we get a match on the name of one of our coffee shops, that should weigh heavier in the relevance than if we got a match on one of our comment bodies.
Since we want to keep our controllers lean and write re-usable code, it's a good idea to move our search logic into its own class. Let's create the file search.rb in our lib folder for this.
include Geokit::Geocoders
class Search
METERS_PER_MILE = 1609.344
SORT_EXPRESSION = "@weight * @weight / @geodist"
def self.execute(keywords, var = {})
@search_options = { :page => var[:page] || 1,
:per_page => CoffeeShop::PER_PAGE }
unless var[:location].blank?
@geocode = MultiGeocoder.geocode(var[:location])
if @geocode.success and @geocode.accuracy > 1
lat = (@geocode.lat / 180.0) * Math::PI
lng = (@geocode.lng / 180.0) * Math::PI
@search_options.merge!(:geo => [lat, lng],
:sort_mode => :expr,
:sort_by => SORT_EXPRESSION,
:with => { "@geodist" => 0.0..(5 * METERS_PER_MILE) })
end
end
CoffeeShop.search(keywords, @search_options)
end
end
That's a lot of awesome. Let's walk through our new search class and see what's going on.
@geodist, @weight and SORT_EXPRESSION
Sphinx gives you some special attributes for sorting and filtering, including @weight and @geodist. @weight is the relevance of a search result (the larger the number, the more relevant the result) and is the default sorting option. @geodist is the distance (in meters) of the search result from the anchor point. By defining SORT_EXPRESSION to use both the @weight and @geodist attributes, we can sort in a way that takes both into account. You can add any other operators and attributes to this expression that you want to tailor how your results are sorted. For instance, if you had a 'popularity' attribute on your model and wanted more popular coffee shops to rank better, you could define your search expression as '@weight * @weight / @geodist + popularity' (just make sure you add 'popularity' to the list of attributes on your model index).
:page and :per_page
If you have WillPaginate installed, Thinking Sphinx will automatically wrap your search results in a WillPaginate collection, allowing you to use all your normal WillPaginate view helpers. Neat!
geocoding
If we pass a :location string to our search class, we want to try to geocode it and use it as an anchor point if the geocoding is successful. We'll also need to convert our lat and lng to radians in order for them to work with Sphinx.
:geo, :sort_mode, :sort_by and :with
These attributes affect the sorting and filtering of our search. :geo tells Sphinx that we are doing a geography search with specific lat and lng variables and tells it to add the @geodist attribute to each result. :sort_mode and :sort_by tell Sphinx that we want to sort results by our SORT_EXPRESSION constant. The :with option tells Sphinx that we only want to return results within five miles of our anchor point.
Finally, the last line performs the actual search based on the keywords and search options we've set up.
Now that we've got our model and search class set up, executing searches from our controller is as simple as:
@coffee_shops = Search.execute(params[:query_keywords],
:page => params[:page],
:location => params[:query_location])
This will execute our query with all the parameters we care about and return a paginated collection of coffee shops sorted by relevancy and distance. Also, if any or all of the parameters are blank, nothing breaks! If all parameters are blank, a Sphinx search won't even be performed, and a WIllPaginate collection will be returned with our model's default sorting and PER_PAGE attributes.
Happy searching!
Joey bookmarked jQuery UI
Joey bookmarked Rediscovering the Button Element
Joey bookmarked Requests Per Second
Joey bookmarked Web Hooks and the Programmable World of Tomorrow
Joey bookmarked The Joel Test: 12 Steps to Better Code
Joey bookmarked Agile Git and the Story Branch Pattern
Joey bookmarked Rails Metal: A Micro-Framework with the Power of Rails
Joey bookmarked Meetup's Dead Simple User Testing
Joey bookmarked Rails 2.2 Released - 27 Links and Resources To Get You Going
Joey bookmarked XML/SWF Charts
Found this great piece of Flash for generating slick charts from XML data. Courtesy of Brian Hogan's Delicious feed.
Joey bookmarked attr_serializable on GitHub
attr_serializable is my first plugin for Rails. It makes it easy to follow best practices and define a whitelist of attributes for each model to use in their to_xml and to_json methods.