I was working with Steph Skardal on the setup of a new DevCamps installation that was going to need to use Ruby 1.9.3, Rails 3, Unicorn and Nginx. This setup was going to be much different than a standard setup due to the different application stack that was required.
The first trick for this was going to get Ruby 1.9.3 on the server. We were using Debian Squeeze but that still only comes with Ruby 1.9.1. We wanted Ruby 1.9.3 for the increased overall speed and significant speed increase with Rails 3. We decided on using rbenv for this task. It's a very easy to setup utility that allows you to maintain multiple version of Ruby in your system user account without the headache of adjusting anything but the PATH environment variable. It takes advantage of another easy to setup utility called ruby build to handle the actual installation of the Ruby source code.
A quick and easy version for setting up a user with this is as follows:Ensure you are in the home directory
cdClone the repository into a .rbenv directory git clone git://github.com/sstephenson/rbenv.git .rbenvAdjust your users path to find the newly installed commands echo "export PATH=$HOME/.rbenv/shims:$HOME/.rbenv/bin:$PATH" >> ~/.bash_profileInstall Ruby version 1.9.3-p0 rbenv install 1.9.3-p0Make Ruby version 1.9.3-p0 your default version every time you log in rbenv global 1.9.3-p0Install the bundler gem for Ruby version 1.9.3-p0 gem install bundlerRefresh rbenv to let it know the new system command bundler exists rbenv rehashNow you are ready to use the bundler gem to install any other gems required for the application.
The normal camps setup assumes you are going to be using Apache for the web server. In this case, we wanted to use Nginx due to memory constraints. We decided to use the proxy capability and just proxy through to Unicorn instead of having to build our own version Nginx to use Passenger. To do this, we had to use a feature in the local-config file in camps that allows you to skip the Apache setup and use your own commands to start, stop and restart your web server and application. Here is the example from our local-config that controlls Nginx and Unicorn. This approach could also be used with Interchange or any other application if you need other services started what mkcamp is run.
skip_apache:1
httpd_start:/usr/sbin/nginx -c __CAMP_PATH__/nginx/nginx.conf
httpd_stop:pid=`cat __CAMP_PATH__/var/run/nginx.pid 2>/dev/null` && kill $pid
httpd_restart:pid=`cat __CAMP_PATH__/var/run/nginx.pid 2>/dev/null` && kill -HUP $pid || /usr/sbin/nginx -c __CAMP_PATH__/nginx/nginx.conf
app_start:__CAMP_PATH__/bin/start-app
app_stop:pid=`cat __CAMP_PATH__/var/run/unicorn.pid 2>/dev/null` && kill $pid
app_restart:pid=`cat __CAMP_PATH__/var/run/unicorn.pid 2>/dev/null` && kill $pid ; sleep 5 ; __CAMP_PATH__/bin/start-appThe contents of the start-app script is simply.cd __CAMP_PATH__ && bundle exec unicorn_rails -c __CAMP_PATH__/config/unicorn.conf.rb -DYou could create one script that handles all aspects of start, stop and restart if you wanted. This setup really wasn't much harder than a normal Ruby on Rails setup. The added time here required to set up rbenv per camp user is offset by the fact that users can manage and try multiple versions of ruby.
Locking hash keys with Hash::Util
It?s a given that you shouldn?t write Perl without ?use strict?; it prevents all kinds of silent bugs involving misspelled and uninitialized variables. A similar aid for misspelled and uninitialized hash keys exists in the module ?Hash::Util?.
By way of background: I was working on a long chunk of code that prepares an e-commerce order for storage in a database. Many of the incoming fields map directly to the table, but others do not. The interface between this code and the page which submits a large JSON structure was in flux for a while, so from time to time I had to chase bugs involving ?missing? or ?extra? fields. I settled on a restricted hash to help me squash these and future bugs.
The idea of a restricted hash is to clamp down on Perl?s rather loose ?record? structure (by which I mean the common practice of using a hash to represent a record with named fields), which is great in some circumstances. While in most programming languages you must pre-declare a structure and live with it, in Perl hashes you can add new keys on the fly, misspellings and all. A restricted hash can only have a particular set of keys, but is still a hash for all other purposes.
An example:
my %hash = (aaa => 1, bbb => 2);
Attempts to reference $hash{ccc} will not return an error, but only an undefined value. We can now lock the hash so that its current roster of keys will be constant:
use Hash::Util qw(lock_keys);
lock_keys(%hash);
and now $hash{ccc} is not only undefined, it?s a run-time error:
$hash{ccc};
Attempt to access disallowed key 'ccc' in a restricted hash
If we know the list of keys before the hash is initialized, we can set it up like this:
my %hash;
lock_keys(%hash, qw(aaa bbb ccc));
Keep in mind the values of $hash{aaa}, etc. are mutable (can be undefined, not exist, scalars, references, etc.), just like a normal hash.
What if our key roster needs to change over the course of the program? In my example, there were several kinds of transactions being sent via JSON, and I needed to validate and restrict fields based on the presence and values of other fields. E.g.,
if ($hash{record_type} eq 'A') {
# validate %hash for aaa, bbb, ccc
}
else {
# validate %hash for aaa, bbb, ddd; ccc should not appear
}
You can add to or modify the accepted keys as you go, but it?s a two-step process: not even Hash::Util can modify the keys of a locked hash, so you have to unlock and re-lock:
my %hash;
lock_keys(%hash, qw(record_type aaa bbb ccc));
# ?
unlock_keys(%hash);
if ($hash{record_type} eq 'A') {
lock_keys(%hash, qw(record_type aaa bbb ccc));
}
else {
lock_keys(%hash, qw(record_type aaa bbb ddd));
}
Of course, that?s kind of wordy: we?d really rather just splice in a key here and there. Hash::Util has you covered, because you can retrieve the list of legal keys for a hash (even if it?s not currently locked):
lock_keys_plus(%hash, qw(ddd));
adds ?ddd? to the list, keeping the previous keys as well. However, if any of the legal keys are not current keys, they won?t make it into the key roster. Instead, use:
lock_keys_plus(%hash, (legal_keys(%hash), qw(more keys here)));
Everything shown here for hashes is also available for hashrefs: for instance, to lock up a hashref $hr:
lock_ref_keys($hr);
unlock_ref_keys($hr);
lock_ref_keys_plus($hr, (legal_ref_keys($hr), qw(other keys)));
Of course, adding all this locking and unlocking adds complexity to your code, so you should consider carefully whether it?s justified. In my case I had 60+ keys, in a nested structure, spanning 1500 lines of code ? I just could not keep all the correct spellings in my head any more, so now when I write
if ($opt->{order_status})
when I mean ?transaction_status?, I?ll get a helpful run-time error instead of a silent skip of that block of code.
Are there other approaches? Yes, depending on your needs: JSON::Schema, for instance, will let you validate a JSON structure against a ?golden master?. However, it does not prevent subsequent assignments to the structure, creating new keys on the fly (possibly in error). Moose would support a restricted object like this, but may add more complexity than you need, so Hash::Util may be the appropriate, lighter-weight approach.
I recently had to build out downloadable product support for a client project running on Piggybak (a Ruby on Rails Ecommerce engine) with extensive use of RailsAdmin. Piggybak's core functionality does not support downloadable products, but it was not difficult to extend. Here are some steps I went through to add this functionality. While the code examples apply specifically to a Ruby on Rails application using paperclip for managing attachments, the general steps here would apply across languages and frameworks.
Data Migration
Piggybak is a pluggable ecommerce engine. To make any models inside your application "sellable", the class method acts_as_variant must be called for any class. This provides a nice flexibility in defining various sellable models throughout the application. Given that I will sell tracks in this example, my first step to supporting downloadable content is adding an is_downloadable boolean and attached file fields to the migration for a sellable item. The migration looks like this:
class CreateTracks < ActiveRecord::Migration
def change
create_table :tracks do |t|
# a bunch of fields specific to tracks
t.boolean :is_downloadable, :nil => false, :default => false
t.string :downloadable_file_name
t.string :downloadable_content_type
t.string :downloadable_file_size
t.string :downloadable_updated_at
end
end
end
Class Definitions
Next, I update my class definition to make tracks sellable and hook in paperclip functionality:
class Track < ActiveRecord::Base
acts_as_variant
has_attached_file :downloadable,
:path => ":rails_root/downloads/:id/:basename.:extension",
:url => "downloads/:id/:basename.:extension"
end
The important thing to note here is that the attached downloadable files must not be stored in the public root. Why? Because we don't want users to access the files via a URL through the public root. Downloadable files will be served via the send_file call, discussed below.
Shipping
Piggybak's order model has_many shipments. In the case of an order that contains only downloadables, shipments can be empty. To accomplish this, I extend the Piggybak::Cart model using ActiveSupport::Concern to check whether or not an order is downloadable, with the following instance method:
module CartDecorator
extend ActiveSupport::Concern
module InstanceMethods
def is_downloadable?
items = self.items.collect { |li| li[:variant].item }
items.all? { |i| i.is_downloadable }
end
end
end
Piggybak::Cart.send(:include, CartDecorator)
If all of the cart items are downloadable, the order is considered downloadable and no shipment is generated for this order. With this cart method, I show the FREE! value on the checkout page under shipping methods.

Forcing Log In
The next step for adding downloadable support is to add code to enforce user log in. In this particular project, I assume that downloads are not included as attachments in files since the files may be extremely large. I add a has_downloadable method used to enforce log in:
module CartDecorator
extend ActiveSupport::Concern
module InstanceMethods
...
def has_downloadable?
items = self.items.collect { |li| li[:variant].item }
items.any? { |i| i.is_downloadable }
end
end
end
Piggybak::Cart.send(:include, CartDecorator)
On the checkout page, a user is forced to log in if cart.has_downloadable?. After log in, the user bounces back to the checkout page.

Download List Page
After a user has purchased downloadable products, they'll need a way to access these files. Next, I create a downloads page which lists orders and their downloads:
With a user instance method (current_user.downloads_by_order), the download index page iterates through orders with downloads to display orders and their downloads. The user method for generating orders and downloads shown here:
class User < ActiveRecord::Base
...
def downloads_by_order
self.piggybak_orders.inject([]) do |arr, order|
downloads = []
order.line_items.each do |line_item|
downloads << line_item.variant.item if line_item.variant.item.is_downloadable?
end
arr << {
:order => order,
:downloads => downloads
} if downloads.any?
arr
end
end
end
The above method would be a good candidate for Rails low-level caching or alternative caching which should be cleared after user purchases to minimize download lookup.
Sending Files
As I mentioned above, download files should not be stored in the public directory for public accessibility. From the download list page, the "Download Now" link maps to the following method in the downloads controller:
class DownloadsController < ApplicationController
def show
item = ProductType.find(params[:id])
if current_user.downloads.include?(item)
send_file "#{Rails.root}/#{item.downloadable.url(:default, false)}"
else
redirect_to(root_url, :notice => "You do not have access to this content.")
end
end
end
Note that there is additional verification here to check if the current user's downloads includes the download requested. The .url(:default, false) bit hides paperclip's cache buster (e.g. "?123456789") from the url in order to send the file.
Conclusion
This straightforward code accomplished the major updates required for download support: storing and sending the file, enforcing login, and handling shipping. In some cases, download support functionality may be more advanced, but the elements described here make up the most basic building blocks.
If you are interested in this project, check out these related articles:
- Introducing Piggybak: A Mountable Ruby on Rails Ecommerce Engine
- ActiveRecord Callbacks for Order Processing in Ecommerce Applications
- Importing into RailsAdmin: Part 1
- Importing into RailsAdmin: Part 2
Or read more about End Point's web development and consulting services!
I recently wrote about importing data in RailsAdmin. RailsAdmin is a Rails engine that provides a nice admin interface for managing your data, which comes packed with configuration options.
In a recent Ruby on Rails ecommerce project, I've been using RailsAdmin, Piggybak (a Rails ecommerce gem supported by End Point), and have been building out custom front-end features such as advanced search and downloadable product support. When this client came to End Point with the project, we offered several options for handling data migration from a legacy system to the new Rails application:
- Create a standard migration file, which migrates data from the existing legacy database to the new data architecture. The advantage with this method is that it requires virtually no manual interaction for the migration process. The disadvantage with this is that it's basically a one-off solution and would never be useful again.
- Have the client manually enter data. This was a reasonable solution for several of the models that required 10 or less entries, but not feasible for the tables containing thousands of entries.
- Develop import functionality to plug into RailsAdmin which imports from CSV files. The advantage to this method is that it could be reused in the future. The disadvantage with ths method is that data exported from the legacy system would have to be cleaned up and formatted for import.
The client preferred option #3. Using a quick script for generating custom actions for RailsAdmin, I developed a new gem called rails_admin_import to handle import that could be plugged into RailsAdmin. Below are some technical details on the generic import solution.
ActiveSupport::Concern
Using ActiveSupport::Concern, the rails_admin_import gem extends ActiveRecord::Base to add the following class methods:
- import_fields: Returns an array of fields that will be included in the import, excluding :id, :created_at, and :updated_at, belongs_to fields, and file fields.
- belongs_to_fields: Returns an array of fields with belongs_to relationships to other models.
- many_to_many_fields: Returns an array of fields with has_and_belongs_to_many relationships to other models.
- file_fields: Returns an array of fields that represent data for Paperclip attached files.
- run_import: Method for running the actual import, receives request params.
And the following instance methods:
- import_files: sets attached files for object
- import_belongs_to_data: sets belongs_to associated data for object
- import_many_to_many_data: sets many_to_many associated data for object
The general approach here is that the import of files, belongs_to, many_to_many relationships, and standard fields makes up the import process for a single object. The run_import method collects success and failure messages for each object import attempt and those results are presented to the user. A regular ActiveRecord save method is called on the object, so the existing validation of objects during each save applies.
Working with Associated Data
One of the tricky parts here is how to handle import of fields representing associations. Given a user model that belongs to a state, country, and has many roles, how would one decide what state, country, or role value to include in the import?
I've solved this by including a dropdown to select the attribute used for mapping in the form. Each of the dropdowns contains a list of model attributes that are used for association mapping. A user can then select the associated mappings when they upload a file. In a real-life situation, I may import the state data via abbreviation, country via display name (e.g. "United States", "Canada") and role via the role name (e.g. "admin"). My data import file might look like this:
| name | favorite_color | state | country | role | |
| Steph Skardal | steph@endpoint.com | blue | CO | United States | admin |
| Aleks Skardal | aleksskardal@gmail.com | green | Norway | user | |
| Roger Skardal | roger@gmail.com | tennis ball yellow | UT | United States | dog |
| Milton Skardal | milton@gmail.com | kibble brown | UT | United States | dog |
Many to Many Relationships
Many to many relationships are handled by allowing multiple columns in the CSV to correspond to the imported data. For example, there may be two columns for role on the user import, where users may be assigned to multiple roles. This may not be suitable for data with a large number of many to many assignments.
Import of File Fields
In this scenario, I've chosen to use open-uri to request existing files from a URL. The CSV must contain the URL for that file to be imported. The import process downloads the file and attaches it to the imported object.
self.class.file_fields.each do |key|
if map[key] && !row[map[key]].nil?
begin
row[map[key]] = row[map[key]].gsub(/s+/, "")
format = row[map[key]].match(/[a-z0-9]+$/)
open("#{Rails.root}/tmp/uploads/#{self.permalink}.#{format}", 'wb') { |file| file << open(row[map[key]]).read }
self.send("#{key}=", File.open("#{Rails.root}/tmp/uploads/#{self.permalink}.#{format}"))
rescue Exception => e
self.errors.add(:base, "Import error: #{e.inspect}")
end
end
end
If the file request fails, an error is added to the object and presented to the user. This method may not be suitable for handling files that do not currently exist on a web server, but it was suitable for migrating a legacy application.
Configuration: Display
Following RailsAdmin's example for setting configurations, I've added the ability to allow the import display to be set for each model.
config.model User do label :name end
The above configuration will yield success and error messages with the user.name, e.g.:
Configuration: Excluded Fields
In addition to allowing a configurable display option, I've added the configuration for excluding fields.
config.model User do
excluded_fields do
[:reset_password_token, :reset_password_sent_at, :remember_created_at,
:sign_in_count, :current_sign_in_at, :last_sign_in_at, :current_sign_in_ip,
:last_sign_in_ip]
end
end
The above configuration will exclude the specified fields during the import, and they will not display on the import page.
Configuration: Additional Fields and Additional Processing
Another piece of functionality that I found necessary for various imports was to hook in additional import functionality. Any model can have an instance method before_import_save that accepts the row of CSV data and map of CSV keys to perform additional tasks. For example:
def before_import_save(row, map) self.created_nested_items(row, map) end
The above method will create nested items during the import process. This simple extensibility allows for additional data to be handled upon import outside the realm of has_and_belongs_to and belongs_to relationships.
Fields for additional nested data can be defined with the extra_fields configuration, and are shown on the import page.
config.model User do
extra_fields do
[:field1, :field2, :field3, :field4]
end
end
Hooking into RailsAdmin
As I mentioned above, I used a script to generate this Engine. Using RailsAdmin configurable actions, import must be added as an action:
config.actions do dashboard index ... import end
And CanCan settings must be updated to allow for import if applicable, e.g.:
cannot :import, :all can :import, User
Conclusion
My goal in developing this tool was to produce reusable functionality that could easily be applied to multiple models with different import needs, and to use this tool across Rails applications. I've already used this gem in another Rails 3.1 project to quickly import data that would otherwise be difficult to deal with manually. The combination of association mapping and configurability produces a flexibility that encourages reusability.
Feel free to review or check out the code here, or read more about End Point's services here.

PostgreSQL functions can be written in many languages. These languages fall into two categories, 'trusted' and 'untrusted'. Trusted languages cannot do things "outside of the database", such as writing to local files, opening sockets, sending email, connecting to other systems, etc. Two such languages are PL/pgSQL and and PL/Perl. For "untrusted" languages, such as PL/PerlU, all bets are off, and they have no limitations placed on what they can do. Untrusted languages can be very powerful, and sometimes dangerous.
One of the reasons untrusted languages can be considered dangerous is that they can cause side effects outside of the normal transactional flow that cannot be rolled back. If your function writes to local disk, and the transaction then rolls back, the changes on disk are still there. Working around this is extremely difficult, as there is no way to detect when a transaction has rolled back at the level where you could, for example, undo your local disk changes.
However, there are times when this effect can be very useful. For example, in a recent thread on the PostgreSQL "general" mailing list (aka pgsql-general), somebody asked for a way to audit SELECT queries into a logging table that would survive someone doing a ROLLBACK. In other words, if you had a function named weapon_details() and wanted to have that function log all requests to it by inserting to a table, a user could simply run the query, read the data, and then rollback to thwart the auditing:
BEGIN;
SELECT weapon_details('BFG 9000'); -- also inserts to an audit table
ROLLBACK; -- inserts to the audit table are now gone!
Certainly there are other ways to track who is using this query, the most obvious being by enabling full Postgres logging (by setting log_statement = 'all' in your postgresql.conf file.) However, extracting that information from logs is no fun, so let's find a way to make that INSERT stick, even if the surrounding function was rolled back.
Stepping back for one second, we can see there are actually two problems here: restricting access to the data, and logging that access somewhere. The ultimate access restriction is to simply force everyone to go through your custom interface. However, in this example, we will assume that someone has psql access and needs to be able to run ad hoc SQL queries, as well as be able to BEGIN, ROLLBACK, COMMIT, etc.
Let's assume we have a table with some Very Important Data inside of it. Further, let's establish that regular users can only see some of that data, and that we need to know who asked for what data, and when. For this example, we will create a normal user named Alice:
postgres=> CREATE USER alice;
CREATE ROLE
We need a way to tell which rows are suitable for people like Alice to view. We will set up a quick classification scheme using the nifty ENUM feature of PostgreSQL:
postgres=> CREATE TYPE classification AS ENUM (
'unclassified',
'restricted',
'confidential',
'secret',
'top secret'
);
CREATE TYPE
Next, as a superuser, we create the table containing sensitive information, and populate it:
postgres=> CREATE TABLE weapon (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
cost TEXT NOT NULL,
security_level CLASSIFICATION NOT NULL,
description TEXT NOT NULL DEFAULT 'a fine weapon'
);
NOTICE: CREATE TABLE will create implicit sequence "weapon_id_seq" for serial column "weapon.id"
NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "weapon_pkey" for table "weapon"
CREATE TABLE
postgres=> INSERT INTO weapon (name,cost,security_level) VALUES
('Crowbar', 10, 'unclassified'),
('M9', 200, 'restricted'),
('M16A2', 300, 'restricted'),
('M4A1', 400, 'restricted'),
('FGM-148 Javelin', 700, 'confidential'),
('Pulse Rifle', 50000, 'secret'),
('Zero Point Energy Field Manipulator', 'unknown', 'top secret');
INSERT 0 7
We don't want anyone but ourselves to be able to access this table, so for safety, we make some explicit revocations. We'll examine the permissions before and after we do this:
postgres=> dp weapon
Access privileges
Schema | Name | Type | Access privileges | Column access privileges
--------+--------+-------+-------------------+--------------------------
public | weapon | table | |
postgres=> REVOKE ALL ON TABLE weapon FROM public;
REVOKE
postgres=> dp weapon
Access privileges
Schema | Name | Type | Access privileges | Column access privileges
--------+--------+-------+---------------------------+--------------------------
public | weapon | table | postgres=arwdDxt/postgres |
As you can see, what the REVOKE really does is remove the implicit "no permission" and grant explicit permissions to only the postgres user to view or modify the table. Let's confirm that Alice cannot do anything with that table:
postgres=> c postgres alice
You are now connected to database "postgres" as user "alice".
postgres=> postgres=> SELECT * FROM weapon;
ERROR: permission denied for relation weapon
postgres=> postgres=> UPDATE weapon SET id = id;
ERROR: permission denied for relation weapon
Alice does need to have access to parts of this table, so we will create a "wrapper function" that will query the table for us and return some results. By declaring this function as SECURITY DEFINER, it will run as if the person who created the function invoked it - in this case, the postgres user. For this example, we'll be letting Alice see the "cost and description" of exactly one item at a time. Further, we are not going to let her (or anyone else using this function) view certain items. Only those items classified as "confidential" or lower can be viewed (i.e. "confidential", "restricted", or "unclassified"). Here's the first version of our function:
postgres=> CREATE LANGUAGE plperlu;
CREATE LANGUAGE
postgres=> CREATE OR REPLACE FUNCTION weapon_details(TEXT)
RETURNS TABLE (name TEXT, cost TEXT, description TEXT)
LANGUAGE plperlu
SECURITY DEFINER
AS $bc$
use strict;
use warnings;
## The item they are looking for
my $name = shift;
## We will be nice and ignore the case and any whitespace
$name =~ s{^s*(S+)s*$}{lc $1}e;
## What is the maximum security_level that people who are
## calling this function can view?
my $seclevel = 'confidential';
## Query the table and pull back the matching row
## We need to differentiate between "not found" and "not allowed",
## by comparing a passed-in level to the security_level for that row.
my $SQL = q{
SELECT name,cost,description,
CASE WHEN security_level <= $1 THEN 1 ELSE 0 END AS allowed
FROM weapon
WHERE LOWER(name) = $2};
## Run the query, pull back the first row, as well as the allowed column value
my $sth = spi_prepare($SQL, 'CLASSIFICATION', 'TEXT');
my $rv = spi_exec_prepared($sth, $seclevel, $name);
my $row = $rv->{rows}[0];
my $allowed = delete $row->{allowed};
## Did we find anything? If not, simply return undef
if (! $rv->{processed}) {
return undef;
}
## Throw an exception if we are not allowed to view this row
if (! $allowed) {
die qq{Sorry, you are not allowed to view information on that weapon!n};
}
## Return the requested data
return_next($row);
$bc$;
CREATE FUNCTION
The above should be fairly self-explanatory. We are using PL/Perl's built-in database access functions, such as spi_prepare, to do the actual querying. Let's confirm that this works as it should for Alice:
postgres=> c postgres alice
You are now connected to database "postgres" as user "alice".
postgres=> SELECT * FROM weapon_details('crowbar');
name | cost | description
---------+------+---------------
Crowbar | 10 | a fine weapon
(1 row)
postgres=> SELECT * FROM weapon_details('anvil');
name | cost | description
------+------+-------------
(0 rows)
postgres=> SELECT * FROM weapon_details('pulse rifle');
ERROR: Sorry, you are not allowed to view information on that weapon!
CONTEXT: PL/Perl function "weapon_details"
Now that we have solved the restricted access problem, let's move on the auditing. We will create a simple table to hold information about who accessed what and when:
postgres=> CREATE TABLE data_audit (
tablename TEXT NOT NULL,
arguments TEXT NULL,
results INTEGER NULL,
status TEXT NOT NULL DEFAULT 'normal',
username TEXT NOT NULL DEFAULT session_user,
txntime TIMESTAMPTZ NOT NULL DEFAULT now(),
realtime TIMESTAMPTZ NOT NULL DEFAULT clock_timestamp()
);
CREATE TABLE
The 'tablename' column simply records which table they are getting data from. The 'arguments' is a free-form field describing what they were looking for. The 'results' column shows how many matching rows were found. The 'status' column will be used primarily to log unusual requests, such as the case where Alice looks for a forbidden item. The 'username' column records the name of the user doing the searching. Because we are using functions with SECURITY DEFINER set, this needs to be session_user, not current_user, as the latter will switch to 'postgres' within the function, and we want to log the real caller (e.g. 'alice'). The final two columns tell us then the current transaction started, and the exact time when an entry was made inside of this table. As a first attempt, we'll have our function do some simple inserts to this new data_audit table:
postgres=> CREATE OR REPLACE FUNCTION weapon_details(TEXT)
RETURNS TABLE (name TEXT, cost TEXT, description TEXT)
LANGUAGE plperlu
SECURITY DEFINER
AS $bc$
use strict;
use warnings;
## The item they are looking for
my $name = shift;
## We will be nice and ignore the case and any whitespace
$name =~ s{^s*(S+)s*$}{lc $1}e;
## What is the maximum security_level that people who are
## calling this function can view?
my $seclevel = 'confidential';
## Query the table and pull back the matching row
## We need to differentiate between "not found" and "not allowed",
## by comparing a passed-in level to the security_level for that row.
my $SQL = q{
SELECT name,cost,description,
CASE WHEN security_level <= $1 THEN 1 ELSE 0 END AS allowed
FROM weapon
WHERE LOWER(name) = $2};
## Run the query, pull back the first row, as well as the allowed column value
my $sth = spi_prepare($SQL, 'CLASSIFICATION', 'TEXT');
my $rv = spi_exec_prepared($sth, $seclevel, $name);
my $row = $rv->{rows}[0];
my $allowed = delete $row->{allowed};
## Log this request
$SQL = 'INSERT INTO data_audit(tablename,arguments,results,status)
VALUES ($1,$2,$3,$4)';
my $status = $rv->{rows}[0] ? $allowed ? 'normal' : 'forbidden' : 'na';
$sth = spi_prepare($SQL, 'TEXT', 'TEXT', 'INTEGER', 'TEXT');
spi_exec_prepared($sth, 'weapon', $name, $rv->{processed}, $status);
## Did we find anything? If not, simply return undef
if (! $rv->{processed}) {
return undef;
}
## Throw an exception if we are not allowed to view this row
if (! $allowed) {
die qq{Sorry, you are not allowed to view information on that weapon!n};
}
## Return the requested data
return_next($row);
$bc$;
However, this fails the case pointed out in the original poster's email about viewing the data within a transaction that is then rolled back. It also fails to work at all when a forbidden item is requested, as that insert is rolled back by the die() call:
postgres=> c postgres alice
You are now connected to database "postgres" as user "alice".
postgres=> SELECT * FROM weapon_details('crowbar');
name | cost | description
---------+------+---------------
Crowbar | 10 | a fine weapon
(1 row)
postgres=> SELECT * FROM weapon_details('pulse rifle');
ERROR: Sorry, you are not allowed to view information on that weapon!
CONTEXT: PL/Perl function "weapon_details"
postgres=> BEGIN;
BEGIN
postgres=> SELECT * FROM weapon_details('m9');
name | cost | description
------+------+---------------
M9 | 200 | a fine weapon
(1 row)
postgres=> ROLLBACK;
ROLLBACK
postgres=> c postgres postgres
You are now connected to database "postgres" as user "postgres".
postgres=> SELECT * FROM data_audit x g
Expanded display is on.
-[ RECORD 1 ]----------------------------
tablename | weapon
arguments | crowbar
results | 1
status | normal
username | alice
txntime | 2012-01-30 17:37:39.497491-05
realtime | 2012-01-30 17:37:39.545891-05
How do we get around this? We need a way to commit something that will survive the surrounding transaction's rollback. The closest thing Postgres has to such a thing at the moment is to connect back to the database with a new and entirely separate connection. Two such popular ways to do so are with the dblink program and the PL/PerlU language. Obviously, we are going to focus on the latter, but all of this could be done with dblink as well. Here are the additional steps to connect back to the database, do the insert, and then leave again:
postgres=> CREATE OR REPLACE FUNCTION weapon_details(TEXT)
RETURNS TABLE (name TEXT, cost TEXT, description TEXT)
LANGUAGE plperlu
SECURITY DEFINER
VOLATILE
AS $bc$
use strict;
use warnings;
use DBI;
## The item they are looking for
my $name = shift;
## We will be nice and ignore the case and any whitespace
$name =~ s{^s*(S+)s*$}{lc $1}e;
## What is the maximum security_level that people who are
## calling this function can view?
my $seclevel = 'confidential';
## Query the table and pull back the matching row
## We need to differentiate between "not found" and "not allowed",
## by comparing a passed-in level to the security_level for that row.
my $SQL = q{
SELECT name,cost,description,
CASE WHEN security_level <= $1 THEN 1 ELSE 0 END AS allowed
FROM weapon
WHERE LOWER(name) = $2};
## Run the query, pull back the first row, as well as the allowed column value
my $sth = spi_prepare($SQL, 'CLASSIFICATION', 'TEXT');
my $rv = spi_exec_prepared($sth, $seclevel, $name);
my $row = $rv->{rows}[0];
my $allowed = defined $row ? delete $row->{allowed} : 1;
## Log this request
$SQL = 'INSERT INTO data_audit(username,tablename,arguments,results,status)
VALUES (?,?,?,?,?)';
my $status = $rv->{rows}[0] ? $allowed ? 'normal' : 'forbidden' : 'na';
my $dbh = DBI->connect('dbi:Pg:service=auditor', '', '',
{AutoCommit=>0, RaiseError=>1, PrintError=>0});
$sth = $dbh->prepare($SQL);
my $user = spi_exec_query('SELECT session_user')->{rows}[0]{session_user};
$sth->execute($user, 'weapon', $name, $rv->{processed}, $status);
$dbh->commit();
## Did we find anything? If not, simply return undef
if (! $rv->{processed}) {
return undef;
}
## Throw an exception if we are not allowed to view this row
if (! $allowed) {
die qq{Sorry, you are not allowed to view information on that weapon!n};
}
## Return the requested data
return_next($row);
$bc$;
CREATE FUNCTION
Note that because we are making external changes, we marked the function as VOLATILE, which ensures that it will always be run every time it is called, and not cached in any form. We are also using a Postgres service file with the 'db:Pg:service=auditor'. This means that the connection information (username, password, database) is contained in an external file. This is not only tidier than hard-coding those values into this function, but safer as well, as the function itself can be viewed by Alice. Finally, note that we are passing the 'username' directly into the function this time, as we have a brand new connection which is no longer linked to the 'alice' user, so we have to derive it ourselves from "SELECT session_user" and then pass it along.
Once this new function is in place, and we re-run the same queries as we did before, we see three entries in our audit table:
postgres=> c postgres postgres
You are now connected to database "postgres" as user "postgres".
Expanded display is on.
-[ RECORD 1 ]----------------------------
tablename | weapon
arguments | crowbar
results | 1
status | normal
username | alice
txntime | 2012-01-30 17:56:01.544557-05
realtime | 2012-01-30 17:56:01.54569-05
-[ RECORD 2 ]----------------------------
tablename | weapon
arguments | pulse rifle
results | 1
status | forbidden
username | alice
txntime | 2012-01-30 17:56:01.559532-05
realtime | 2012-01-30 17:56:01.561225-05
-[ RECORD 3 ]----------------------------
tablename | weapon
arguments | m9
results | 1
status | normal
username | alice
txntime | 2012-01-30 17:56:01.573335-05
realtime | 2012-01-30 17:56:01.574989-05
So that's the basic premise of how to solve the auditing problem. For an actual production script, you would probably want to cache the database connection by sticking things inside of the special %_SHARED hash available to PL/Perl and Pl/PerlU. Note that each user gets their own version of that hash, so Alice will not be able to create a function and have access to the same %_SHARED hash that the postgres user has access to. It's probably a good idea to simply not let users like Alice use the language at all. Indeed, that's the default when we do the CREATE LANGUAGE call as above:
postgres=> c postgres alice
You are now connected to database "postgres" as user "alice".
postgres=> CREATE FUNCTION showplatform()
RETURNS TEXT
LANGUAGE plperlu
AS $bc$
return $^O;
$bc$;
ERROR: permission denied for language plperlu
Further refinements to the actual script might include refactoring the logging bits to a separate function, writing some of the auditing data to a file on the local disk, recording the actual results returned to the user, and sending the data to another Postgres server entirely. For that matter, as we are using DBI, you could send it to other place entirely - such as a MySQL, Oracle, or DB2 database!
Another place for improvement would be associating each user with a security_level classification, such that any user could run the function and only see things at or below their level, rather than hard-coding the level as "confidential" as we have done here. Another nice refinement might be to always return undef (no matches) for items marked "top secret", to prevent the very existence of a top secret weapon from being deduced. :)
Private mount points with unshare
Linux offers some pretty interesting features that are either new, borrowed, obscure, experimental, or any combination of those qualities. One such feature that is interesting is the unshare() function, which the unshare(2) man page says ?allows a process to disassociate parts of its execution context that are currently being shared with other processes. Part of the execution context, such as the mount namespace, is shared implicitly when a new process is created using fork(2) or vfork(2)?.
I?m going to talk here about one option to unshare: per-process private filesystem mount points, also described as mount namespaces. This Linux kernel feature has been around for a few years and is easily accessible in the userland command unshare(1) in util-linux-ng 2.17 or newer (which is now simply util-linux again without the "ng" distinction because the fork took over mainline development).
Running `unshare -m` gives the calling process a private copy of its mount namespace, and also unshares file system attributes so that it no longer shares its root directory, current directory, or umask attributes with any other process.
Yes, completely private mount points for each process. Isn?t that interesting and strange?
A demonstration
Here?s a demonstration on an Ubuntu 11.04 system. In one terminal:
% su -
Password:
# unshare -m /bin/bash
# secret_dir=`mktemp -d --tmpdir=/tmp`
# echo $secret_dir
/tmp/tmp.75xu4BfiCw
# mount -n -o size=1m -t tmpfs tmpfs $secret_dir
# df -hT
Filesystem Type Size Used Avail Use% Mounted on
/dev/mapper/auge-root
ext4 451G 355G 74G 83% /
There?s no system-wide sign of /tmp/tmp.* there thanks to mount -n which hides it. But it can be seen process-private here:
# grep /tmp /proc/mounts tmpfs /tmp/tmp.75xu4BfiCw tmpfs rw,relatime,size=1024k 0 0 # cd $secret_dir # ls -lFa total 36 drwxrwxrwt 2 root root 40 2011-11-03 22:10 ./ drwxrwxrwt 21 root root 36864 2011-11-03 22:10 ../ # touch play-file # mkdir play-dir # ls -lFa total 36 drwxrwxrwt 3 root root 80 2011-11-03 22:10 ./ drwxrwxrwt 21 root root 36864 2011-11-03 22:10 ../ drwxr-xr-x 2 root root 40 2011-11-03 22:10 play-dir/ -rw-r--r-- 1 root root 0 2011-11-03 22:10 play-file
Afterward, in another terminal, and thus a separate process with no visibility into the above-shown terminal process?s private mount points:
% su - Password: # grep /tmp /proc/mounts # cd /tmp/tmp.75xu4BfiCw # ls -lFa total 40 drwx------ 2 root root 4096 2011-11-03 22:10 ./ drwxrwxrwt 21 root root 36864 2011-11-03 22:18 ../
It?s all secret!
Use cases
This feature makes it possible for us to create a private temporary filesystem that even other root-owned processes cannot see or browse through, raising the bar considerably for a naive attacker to get access to sensitive files or even see that they exist, at least when they?re not currently open and visible to e.g. lsof.
Of course a sophisticated attacker would presumably have a tool to troll through kernel memory looking for what they need. As always, assume that a sophisticated attacker who has access to the machine will sooner or later have anything they really want from it. But we?d might as well make it a challenge.
Another possible use of this feature is to have a process unmount a filesystem privately, perhaps to reduce the exposure of other files on a system to a running daemon if it is compromised.
/etc/mtab vs. /proc/mounts
Experimenting with this feature also drew my attention to differences in how popular Linux distributions expose mount points. There are actually traditionally two places that the list of mounts is stored on a Linux system.
First, the classic Unix /etc/mtab, which is in essence a materialized view. It is the reason that on the Ubuntu 11.04 example above we see the private mount point everywhere on the system, but it reported different disk sizes. The existence of the mount point was global in /etc/mtab but the sizes are determined dynamically and differ based on process?s view into the mount points themselves. The `mount -n` option tells mount to not put the new mount point into /etc/mtab. And this is what the df(1) command refers to. How repulsive that a file in the normally read-only /etc is written to so nonchalantly!
Second, the Linux-specific /proc/mounts, which is real-time, exact, and accurate, and can appear differently to each process. The mount invocation can?t hide anything from /proc/mounts. This is what you would think is the only place to look for mounts, but /etc/mtab is still used some places.
Ubuntu 11.04 still has both, with a separate /etc/mtab. Fedora 16 has done away with /etc/mtab entirely and made it merely a symlink to /proc/mounts, which makes sense, but that is a newer convention and leads to the surprising difference here.
Linux distributions and unshare
The unshare userland command in util-linux(-ng) comes with RHEL 6, Debian 6, Ubuntu 11.04, and Fedora 16, but not on the very common RHEL 5 or CentOS 5. Because we needed it on RHEL 5, I made a simple package that contains only the unshare(1) command and peacefully coexists with the older stock RHEL 5 util-linux. It?s called util-linux-unshare and here are the RPM downloads for RHEL 5:
- x86_64: util-linux-unshare-2.20.1-3.ep.x86_64.rpm
- i386: util-linux-unshare-2.20.1-3.ep.i386.rpm
- SRPM: util-linux-unshare-2.20.1-3.ep.src.rpm
I hope you?ve found this as interesting as I did!
Further reading
- Karel Zak is the util-linux maintainer and a Red Hat employee; see his detailed blog post about the unshare command
- unshare(2) function man page
- unshare(1) userland command man page
- The difference between /etc/mtab and /proc/mounts is described well in Karel Zak?s blog post about bind mounts
- util-linux overview
We do a lot of our hosting at SoftLayer, which seems to be one of the hosts with the most servers in the world -- they claim to have over 100,000 servers as of last month. More important for us than sheer size are many other fine attributes that SoftLayer has, in no particular order:
- a strong track record of reliability
- responsive support
- datacenters around the U.S. and some in Europe and Asia
- solid power backup
- well-connected redundant networks with multiple 10 Gbps uplinks
- gigabit Ethernet pipes all the way to the Internet
- first-class IPv6 support
- an internal private network with no data transfer charge
- Red Hat Enterprise Linux offered at no extra charge
- diverse dedicated server offerings at many price & performance points
- some disk partitioning options (though more flexibility here would be nice, especially with LVM for the /boot and / filesystems)
- fully automated provisioning, without salesman & quote hassles for standard offerings
- 3000 GB data transfer per month included standard with most servers
- month-to-month contracts
- reasonable prices (though we can of course always use lower prices, we'll take quality over cheapness for most of our hosting needs!)
- no arbitrary port blocks (some other providers rate-limit incoming TCP connections on port 22 to slow down ssh dictionary attacks, while others forbid IRC, etc.)
- a web service API for monitoring and controlling many aspects of our account via REST/JSON or SOAP
(No, they're not paying me for writing this! But they really have nice offerings.)
It is this last item, the SoftLayer API, that I want to elaborate on here.
The SoftLayer Development Network features API information and documentation and once you have an API account set up in the management website (quick and easy to do), you can start automating all sorts of tasks, from provisioning new hosts, monitoring your upcoming invoice or other accounting information, and much more.
I've released as open source two scripts we use: One is for managing secondary DNS domains in SoftLayer's DNS servers, from a primary name server running BIND 9. The other is a Nagios check script for monitoring monthly data transfer used and alerting when over a set threshold or over the monthly allotment.
See the GitHub repository of endpoint-softlayer-api if they would be useful to you, or to use as a starting point to interface with other SoftLayer APIs.
If you're using MySQL replication, then you're probably counting on it for some fairly important need. Monitoring via Nagios is generally considered a best practice. This article assumes you've already got your Nagios server setup and your intention is to add a Ubuntu 10.04 NRPE client. This article also assumes the Ubuntu 10.04 NRPE client is your MySQL replication master, not the slave. The OS of the slave does not matter.
Getting the Nagios NRPE client setup on Ubuntu 10.04
At first it wasn't clear what packages would be appropriate packages to install. I was initially mislead by the naming of the nrpe package, but I found the correct packages to be:
sudo apt-get install nagios-nrpe-server nagios-plugins
The NRPE configuration is stored in /etc/nagios/nrpe.cfg, while the plugins are installed in /usr/lib/nagios/plugins/ (or lib64). The installation of this package will also create a user nagios which does not have login permissions. After the packages are installed the first step is to make sure that /etc/nagios/nrpe.cfg has some basic configuration.
Make sure you note the server port (defaults to 5666) and open it on any firewalls you have running. (I got hung up because I forgot I have both a software and hardware firewall running!) Also make sure the server_address directive is commented out; you wouldn't want to only listen locally in this situation. I recommend limiting incoming hosts by using your firewall of choice.
Choosing what NRPE commands you want to support
Further down in the configuration, you'll see lines like command[check_users]=/usr/lib/nagios/plugins/check_users -w 5 -c 10. These are the commands you plan to offer the Nagios server to monitor. Review the contents of /usr/lib/nagios/plugins/ to see what's available and feel free to add what you feel is appropriate. Well designed plugins should give you a usage if you execute them from the command line. Otherwise, you may need to open your favoriate editor and dig in!
After verifying you've got your NRPE configuration completed and made sure to open the appropriate ports on your firewall(s), let's restart the NRPE service:
service nagios-nrpe-server restart
This would also be an appropriate time to confirm that the nagios-nrpe-server service is configured to start on boot. I prefer the chkconfig package to help with this task, so if you don't already have it installed:
sudo apt-get install chkconfig chkconfig | grep nrpe # You should see... nagios-nrpe-server on # If you don't... chkconfig nagios-nrpe-server on
Pre flight check - running check_nrpe
Before going any further, log into your Nagios server and run check_nrpe and make sure you can execute at least one of the commands you chose to support in nrpe.cfg. This way, if there are any issues, it is obvious now, while we've not started modifying your Nagios server configuration. The location of your check_nrpe binary may vary, but the syntax is the same:
check_nrpe -H host_of_new_nrpe_client -c command_name
If your command output something useful and expected, your on the right track. A common error you might see: Connection refused by host. Here's a quick checklist:
- Did you start the nagios-nrpe-server service?
- Run
netstat -aunton the NRPE client to make sure the service is listening on the right address and ports. - Did you open the appropriate ports on all your firewall(s)?
- Is there NAT translation which needs configuration?
Adding the check_mysql_replication plugin
There is a lot of noise out there on Google for Nagios plugins which offer MySQL replication monitoring. I wrote the following one using ideas pulled from several existing plugins. It is designed to run on the MySQL master server, check the master's log position and then compare it to the slave's log position. If there is a difference in position, the alert is considered Critical. Additionally, it checks the slave's reported status, and if it is not "Waiting for master to send event", the alert is also considered critical. You can find the source for the plugin at my Github account under the project check_mysql_replication. Pull that source down into your plugins directory (/usr/lib/nagios/plugins/ (or lib64)) and make sure the permissions match the other plugins.
With the plugin now in place, add a command to your nrpe.cfg.
command[check_mysql_replication]=sudo /usr/lib/nagios/plugins/check_mysql_replication.sh -H
At this point you may be saying, WAIT! How will the user running this command (nagios) have login credentials to the MySQL server? Thankfully we can create a home directory for that nagios user, and add a .my.cnf configuration with the appropriate credentials.
usermod -d /home/nagios nagios #set home directory mkdir /home/nagios chmod 755 /home/nagios chown nagios:nagios /home/nagios # create /home/nagios/.my.cnf with your preferred editor with the following: [client] user=example_replication_username password=replication_password chmod 600 /home/nagios/.my.cnf chown nagios:nagios /home/nagios/.my.cnf
This would again be an appropriate place to run a pre flight check and run the check_nrpe from your Nagios server to make sure this configuration works as expected. But first we need to add this command to the sudoer's file.
nagios ALL= NOPASSWD: /usr/lib/nagios/plugins/check_mysql_replication.sh
Wrapping Up
At this point, you should run another check_nrpe command from your server and see the replication monitoring report. If not, go back and check these steps carefully. There are lots of gotchas and permissions and file ownership are easily overlooked. With this in place, just add the NRPE client using the existing templates you have for your Nagios servers and make sure the monitoring is reporting as expected.
Update: Read an update to this functionality here.
I've blogged about RailsAdmin a few times lately. I've now used it for several projects, and have included it as a based for the Admin interface my recent released Ruby on Rails Ecommerce Engine (Piggybak). One thing that I found lacking in RailsAdmin is the ability to import data. However, it has come up in the RailsAdmin Google Group and it may be examined in the future. One problem with developing import functionality is that it's tightly coupled to the data and application logic, so building out generic import functionality may need more thought to allow for elegant extensibility.
For a recent ecommerce project using RailsAdmin and Piggybak, I was required to build out import functionality. The client preferred this method to writing a simple migration to migrate their data from a legacy app to the new app, because this import functionality would be reusable in the future. Here are the steps that I went through to add Import functionality:
#1: Create Controller
class CustomAdminController < RailsAdmin::MainController
def import
# TODO
end
end
First, I created a custom admin controller for my application in the app/controllers/ directory that inherits from RailsAdmin::MainController. This RailsAdmin controller has several before filters to set the required RailsAdmin variables, and defines the correct layout.
#2: Add import route
match "/admin/:model_name/import" => "custom_admin#import" , :as => "import", :via => [:get, :post] mount RailsAdmin::Engine => '/admin', :as => 'rails_admin'
In my routes file, I introduced a new named route for import to point to the new custom controller. This action will be a get or a post.
#3: Override Rails Admin View
Next, I copied over the RailsAdmin app/views/rails_admin/_model_links.html.haml view to my application to override RailsAdmin's view. I made the following addition to this file:
...
- can_import = authorized? :import, abstract_model
...
%li{:class => (params[:action] == 'import' && 'active')}= link_to "Import", main_app.import_path(model_name) if can_import
With this logic, the Import tab shows only if the user has import access on the model. Note that the named route for the import must be prefixed with "main_app.", because it belongs to the main application and not RailsAdmin.
#4: CanCan Settings
My application uses CanCan with RailsAdmin, so I leveraged CanCan to control which models are importable. The CanCan Ability class (app/models/ability.rb) was updated to contain the following, to allow exclude import on all models, and then allow import on several specific models.
if user && user.is_admin? cannot :import, :all can :import, [Book, SomeModel1, SomeModel2, SomeModel3] end
I now see an Import tab in the admin:

#5: Create View
Next, I created a view for displaying the import form. Here's a generic example to display the set of fields that can be imported, and the form:
<h1>Import</h1>
<h2>Fields</h2>
<ul>
<% @abstract_model::IMPORT_FIELDS.each do |attr| -%>
<li><%= attr %></li>
<% end -%>
</ul>
<%= form_tag "/admin/#{@abstract_model.to_param}/import", :multipart => true do |f| -%>
<%= file_field_tag :file %>
<%= submit_tag "Upload", :disable_with => "Uploading..." %>
<% end -%>
This will look something like this:

#6: Import Functionality
Finally, the code for the import looks something like this:
def import
if request.post?
response = { :errors => [], :success => [] }
file = CSV.new(params[:file].tempfile)
# Build map of attributes based on first row
map {}
file.readline.each_with_index { |key, i| map[key.to_sym] = i }
file.each do |row|
# Build hash of attributes
new_attrs = @abstract_model.model::IMPORT_FIELDS.inject({}) { |hash, a| hash[a] = row[map[a]] if map[a] }
# Instantiate object
object = @abstract_model.model.new(new_attrs)
# Additional special stuff here
# Save
if object.save
response[:success] << "Created: #{object.title}"
else
response[:error] << "Failed to create: #{object.title}. Errors: #{object.errors.full_messages.join(', ')}."
end
end
end
end
Note that a hash of keys and locations is created to map keys to the columns in the imported file. This allows for flexibility in column ordering of imported files. Later, I'd like to to re-examine the CSV documentation to identify if there is a more elegant way to handle this.
#7: View updates to show errors
Finally, I update my view to show both success and error messages, which looks sorta like this in the view:

Conclusion and Discussion
It was pretty straightforward to get this figured out. The only disadvantage I see to this method is that overriding the rails_admin view requires recopying or manual updates to the view over during upgrades of the gem. For example, if any part of the rails_admin view has changes, those changes must also be applied to the custom view. Everything else should be smooth sailing :)
In reality, my application has several additional complexities, which make it less suitable for generic application:
- Several of the models include attached files via paperclip. Using open-uri, these files are retrieved and added to the objects.
- Several of the models include relationships to existing models. The import functionality requires lookup of these associated models (e.g. an imported book belongs_to an existing author), and reports and error if the associated objects can not be found.
- Several of the models require creation of a special nested object. This was model specific.
- Because of this model specific behavior, the import method is moved out of the controller into model-specific class methods. For example, CompactDisc.import is different from Book.import which is different from Track.import. Pulling the import into a class method also makes for a skinnier controller here.
Read more about End Point's Ruby on Rails development and consulting services or contact us to help you out with a Rails project today!
Recently, I posted about how to import comments from a Ruby on Rails app to Disqus. This is a follow up to that post where I outline the implementation of Disqus in a Ruby on Rails site. Disqus provides what it calls Universal Code which can be added to any site. This universal code is just JavaScript, which asynchronously loads the Disqus thread based on one of two unique identifiers Disqus uses.
Disqus in a development environment
Before we get started, I'd recommend that you have two Disqus "sites"; one for development and one for production. This will allow you to see real content and experiment with how things will really behave once you're in production. Ideally, your development server would be publicly accessible to allow you to fully use the Disqus moderation interface, but it isn't required. Simply register another Disqus site, and make sure that you have your shortname configured by environment. Feel free to use whatever method you prefer for defining these kinds of application preferences. If you're looking for an easy way, considering checking out my article on Working with Constants in Ruby. It might look something like this:
# app/models/article.rb DISQUS_SHORTNAME = Rails.env == "development" ? "dev_shortname".freeze : "production_shortname".freeze
Disqus Identifiers
Each time you load the universal code, you need to specify a few configuration variables so that the correct thread is loaded:
- disqus_shortname: tells Disqus which website account (called a forum on Disqus) this system belongs to.
- disqus_identifier: tells Disqus how to uniquely identify the current page.
- disqus_url: tells Disqus the location of the page for permalinking purposes.
# app/views/disqus/_thread.html.erb
# assumes you've passed in the local variable 'article' into this partial
# from http://docs.disqus.com/developers/universal/
<div id="disqus_thread"></div>
<script type="text/javascript">
var disqus_shortname = '<%= Article::DISQUS_SHORTNAME %>';
var disqus_identifier = '<%= article.id %>';
var disqus_url = '<%= url_for(article, :only_path => false) %>';
/* * * DON'T EDIT BELOW THIS LINE * * */
(function() {
var dsq = document.createElement('script'); dsq.type = 'text/javascript'; dsq.async = true;
dsq.src = 'http://' + disqus_shortname + '.disqus.com/embed.js';
(document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]).appendChild(dsq);
})();
</script>
The above code will populate the div#disqus_thread with the correct content based on your disqus_identifier. By setting up a single partial that will always render your threads, it becomes very easy to adjust this code if needed.
Disqus Identifier Gotcha
We found during our testing a surprising and unexpected behavior in how Disqus associates a thread to a URL. In our application, the landing page was designed to show the newest article as well as the Disqus comments thread. We found that once a new article was posted, the comments from the previous article were still shown! It seems Disqus ignored the unique disqus_identifier we had specified and instead associated the thread with the landing page URL. In our case, a simple routing change allowed us to forward the user to the unique URL for that content and thread. In your case, there may not be such an easy work around, so be certain you include both the disqus_identifier and disqus_url JavaScript configuration variables above to minimize the assumptions Disqus will make. When at all possible, always use unique URLs for displaying Disqus comments.
Comment Counters
Often an index page will want to display a count of how many comments are in a particular thread. Disqus uses the same asynchronous approach to loading comment counts. Comment counts are shown by adding code such as the following where you want to display your count:
# HTML
<a href="http://example.com/article1.html#disqus_thread"
data-disqus-identifier="<%=@article.id%>">
This will be replaced by the comment count
</a>
# Rails helper
<%= link_to "This will be replaced by the comment count",
article_path(@article, :anchor => "disqus_thread"),
:"data-disqus-identifer" => @article.id %>
At first this seemed strange, but it is the exact same pattern used to display the thread. It would likely be best to remove the link text so nothing is shown until the comment count is loaded, but I felt for my example, having some meaning to the test would help understanding. Additionally, you'll need to add the following JavaScript to your page.
# app/view/disqus/_comment_count_javascript.html.erb
# from http://docs.disqus.com/developers/universal/
# add once per page, just above </body>
<script type="text/javascript">
var disqus_shortname = '<%= Article::DISQUS_SHORTNAME %>';
/* * * DON'T EDIT BELOW THIS LINE * * */
(function () {
var s = document.createElement('script'); s.async = true;
s.type = 'text/javascript';
s.src = 'http://' + disqus_shortname + '.disqus.com/count.js';
(document.getElementsByTagName('HEAD')[0] || document.getElementsByTagName('BODY')[0]).appendChild(s);
}());
</script>
Disqus recommends adding it just before the closing </body> tag. You only need to add this code ONCE per page, even if you're planning on showing multiple comment counts on a page. You will need this code on any page with a comment count, so I do recommend putting it in a partial. If you wanted, you could even include it in a layout.
Styling Comment Counts
Disqus provides extensive CSS documentation for its threads, but NONE for its comment counters. In our application, we had some very particular style requirements for these comment counts. I found that in Settings > Appearance, I could add HTML tags around the output of the comments.
This allowed me to style my comments as needed, although these fields are pretty small, so make sure to compress your HTML as much as possible.
A Set of Tenets I Have Set Myself
- If You Do Not Pay for It, You Do Not Own It
- You Have the Right to Help, Not the Right to Complain
- It Is Easier to Do Without, Than It Is to Do Away With
- What I Must Therefore Do
This is something that has been on my mind for quite a while, but if I do not commit it to bytes then it will continue to linger, unresolved.
I know that I cannot communicate these ideals without even myself knowing that I am culpable of hypocrisy. That said, hypocrisy is not the mere doing opposite of what one says, it is the wilful ignorance thereof. When one sees one?s own ignorance and yet does not correct it, that is hypocrisy. I am compelled by love to change and so I must air what I wish to be and then I must become it, even if I may appear on the surface a hypocrite.
Here then are three tenets I have set myself for living digitally in this modern age. They come from self-reflection on the way the digital landscape has changed (and how I have changed) in the last 10 years. We are not living in the same world as before and we must realise, and change with it.
Your results may not be the same, but I advise you to do likewise.
If You Do Not Pay for It, You Do Not Own It
You are the product, not the customer. The ad men are the customers of Google, Facebook, Twitter et al. You are a number, a statistic, a collection of?essentially?SEO-spam aimed to generate ?value? to these companies.
The data you put into these companies is theirs, not yours. You might think it is yours but you don?t have access to the hard disk in their organisation where your data is stored. You cannot ?backup? your Facebook / Twitter, you cannot copy it to somewhere else. You cannot examine it, modify it or do anything meaningful with it by yourself using your own computing power. If you can?t FTP into these ?clouds? and delete files then you don?t own nor control them. Don?t be fooled by an interface.
What I will learn from this is to not use services unless I am a paying customer, accountable to my own data, and copying data into these services rather than creating data inside them.
You Have the Right to Help, Not the Right to Complain
Where does open source fit into this? Of course you don?t pay for open source or gratis content (you don?t pay to read this website for starters), so if you don?t own it who should you pay? The fact is that content provided under a free / liberal licence is owned by you. It is a right automatically given to you by the owner for accessing that content. For example, by reading this article (it has been downloaded to your browser?s cache), you own it. The fact that almost every person who reads this article won?t do anything with it is by-the-by and not my problem, but the fact remains that by merely accessing the article it has become your personal property to do with as you please within the boundaries of the licence and the law (e.g. fair use).
The substantial cost of content that is given to you under such free / liberal licences has been paid for you. It is my recommendation therefore to pay back such acts of kindness with either contributing meaningfully wherein your capability (submitting patches, filing bugs, suggesting ideas or sending a thankful e-mail) or where not possible or not within your capability, making a donation, or in the case of shareware, buying a licence (even if it?s not required of you)
I am very guilty of complaining. It is far too easy to complain about something which you paid nothing for because its value to you is based on its practical use (does it work?), not on an agreed standard of value?money?that is, ?I paid for this and it doesn?t do what it says? vs. ?your program sucks?.
The advice I am giving and will be following is?where possible?to be careful thoughtful and attempt to avoid receiving or ?getting?, as it were new things without first being prepared to give.
In practical terms, this means limiting the amount of RSS I subscribe to and reviewing what websites I ?waste time? on. If I view such time-wasting content with the eye that I should be prepared to give something in return for all of it (even if it?s just leaving comments), I will be more careful about quite how much time wasting stuff I waste my time on. The idea being that instead of watching tons of crap and not being bothered to comment on any of it, I should be watching a little and commenting on all of it.
It Is Easier to Do Without, Than It Is to Do Away With
Paying for things is a good way of determining what really matters (on a material level) in your life. Paying for things you don?t have to, even more so. You see, pirating TV shows or films may very well get you a lot of entertainment, but it is much harder to judge what is not important and can be lived without when you have an excess of content that you did not pay for. Overall, I have discovered that it is better to live without most content than to live with piles of it.
That does not mean that I am adverse to paying for value. Whereas I'm not willing to pay £20 for a Hollywood movie on DVD, I will happily give £100 to support someone making stuff I want to watch (BTW, check out BBS: The Documentary, an 8-part series on what existed before the World Wide Web, available free?I bought the DVDs for a friend).
-
Do not sign up for new services without first considering the exit-strategy
- How much work is involved in you moving away from the service?
- Will you be creating data within the service?
-
Services for which you cannot choose to pay are the ones to be most avoided
-
Do not ignore your right to choose and don?t underestimate its power. You do have a choice what services you use. You are not ?forced? to use Twitter, Facebook or Google+. No force in this universe lifts your finger to click the sign-up button but you, and you alone. Exercise your power to choose to say ?no? even if on a professional level it may appear to be a detriment, you will be greatly enriched on a professional level by learning to say ?no?.
What I Must Therefore Do
I have some very hard actions to face up to now.
- GitHub
-
I need to pay for my GitHub usage. Sure it?s given to me free but if I'm not paying them, what am I? A customer? No. I'm a free advert. It?s a really good service, but I'd rather be in a professional relationship with companies than an informal one where they can do what they want to me and I'm worth nothing to them (notice how companies like Facebook and Twitter treat their users when it comes to site redesigns).
In the long term, should money be an issue or GitHub fail to serve my needs, I could conceivably migrate to running my own git server. This is one of the good things about git and GitHub, that the git repository is a complete backup of the entire history of the project?s code and therefore you are not locked into GitHub in any real means, besides URLs that people link to. Imagine if Facebook or Twitter worked like git / GitHub? You could download a file and upload it to a different service and your entire history would be perfectly imported and preserved; but where?s the profit in that?
- GMail
-
I could pay for a Google apps account (it?s not expensive), but the way Google is going (destroying the quality of their search with social-network junk) I'd rather move away from them completely long-term. I've switched to DuckDuckGo as my primary search engine and fall back on Google for anything it can?t handle, but I'm inclined to give Bing a try.
Migrating away from GMail is going to be one of the hardest things to do (should have thought about that many years ago, but there you go?we live and learn). I mainly use it because of the superior spam detection. I may have to get used to the idea of several hundred e-mails being downloaded to my local client and spending some time training up its filters. I aim to forward my Google e-mail for a year and then delete all of my Google accounts.
-
Ah, the biggie. I have a love / hate / hate relationship with Twitter. I don?t believe in being a digital hermit?technology when available (even with caveats) should be used to primarily help others. However, it?s clear what Twitter?s game is. They?ve become successful and now want to control the market to prevent the natural technology cycle that will inevitably destroy their business model.
I had decided to build a replacement for Twitter, but abandoned that because I had made the mistake of being too enamoured with the idea, rather than the execution. I'm thinking I should follow the approach I took with NoNonsense Forum; get something working with as few lines as possible and just polish over time, rather than trying to create the end-product from the beginning.
I'll continue using Twitter in the future, but I'm going to aim to move my architecture so that I'm publishing my data into it rather than trying to get data out of it.
- YouTube
-
I watch very little commercial TV. When I am married my wife and I won?t require a TV licence (a tax in the UK for accessing television broadcasts which is used to fund the non-profit BBC.) I'll be very glad to be shot of the TV, 99% of the stuff on it is utter junk. However, generally speaking, I like the idea of the TV licence because it means that I am paying more directly for the content to be produced, rather than paying a subscription to a cable company who has complex relationships with the companies who actually produce and sell the content and still have the gaul to fill their air-time with adverts (the BBC do not run adverts).
The majority of my ?TV? entertainment is a few YouTube users. These independent, amateur, un-funded, non-commercial and totally normal people with low-resolution, barely-edited videos provide me far more enjoyment than everything commercial TV has to offer. If you?re interested in Minecraft, then I recommend watching EthosLab, Docm77 and Zisteau.
These YouTube users receive some recompense through advertising, which I block on all sites (that, and NoScript) for reasons of basic internet security. Therefore I am not opposed to donating to these individuals, but I have decided to be much less sporadic with donations and find a good value-measurement that I can use to pay these authors for their content.
I have decided to start out by working with a $1-per-video-I-watch donation which I will accrue and pay at the end of each month (About $40 by my estimate). Of course it?s not for me to decide how much money the author believes his content is worth if he were to charge for it, but I believe it?s far better for me to be donating regularly what I think it?s worth than just expecting something for nothing.
These tenets are not demands on how others should conduct themselves, they are my own personal conclusions and I hope that others can look at this exercise and consider their own relationship with digital services. It?s a proven fact that material wealth does not bring happiness and I'm pretty certain digital wealth doesn?t either. In my opinion it is better to minimise your digital wealth so that it does not draw you away from focusing on (and developing) a love of people.
I'll be interested in hearing your opinion on this topic. You can drop a comment about it in my anonymous,
no-registration-required forum, or e-mail
kroccamen@gmail.com
kroc@camendesign.com.
One day I was reading through the documentation on search.cpan.org for the DBI module and ran across an attribute that you can use with selectall_arrayref() that creates the proper data structure to be used with Interchange's object.mv_results loop attribute. The attribute is called Slice which causes selectall_arrayref() to return an array of hashrefs instead of an array of arrays. To use this you have to be working in global Perl modules as Safe.pm will not let you use the selectall_arrayref() method.
An example of what you could use this for is an easy way to generate a list of items in the same category. Inside the module, you would do like this:
my $results = $dbh->selectall_arrayref(
q{
SELECT
sku,
description,
price,
thumb,
category,
prod_group
FROM
products
WHERE
category = ?},
{ Slice => {} },
$category
);
$::Tag->tmpn("product_list", $results);
In the actual HTML page, you would do this:
<table cellpadding=0 cellspacing=2 border=1>
<tr>
<th>Image</th>
<th>Description</th>
<th>Product Group</th>
<th>Category</th>
<th>Price</th>
</tr>
[loop object.mv_results=`$Scratch->{product_list}` prefix=plist]
[list]
<tr>
<td><a href="/cgi-bin/vlink/[plist-param sku].html"><img src="[plist-param thumb]"></a></td>
<td>[plist-param description]</td>
<td>[plist-param prod_group]</td>
<td>[plist-param category]</td>
<td>[plist-param price]</td>
</tr>
[/list]
[/loop]
</table>
We normally use this when writing ActionMaps and using some template as our setting for mv_nextpage.
As I recently blogged about, I introduced a new Ruby on Rails Ecommerce Engine. The gem relies on RailsAdmin, a Ruby on Rails engine that provides a nice interface for managing data. Because the RailsAdmin gem drives order creation on the backend in the context of a standard but configurable CRUD interface, and because I didn't want to hack at the RailsAdmin controllers, much of the order processing logic leverages ActiveRecord callbacks for processing. In this blog article, I'll cover the process that happens when an order is saved.
Order Data Model
The first thing to note is the data model and the use of nested attributes. Here's how the order model relates to its associated models:
class Order < ActiveRecord::Base has_many :line_items, :inverse_of => :order has_many :payments, :inverse_of => :order has_many :shipments, :inverse_of => :order has_many :credits, :inverse_of => :order belongs_to :billing_address, :class_name => "Piggybak::Address" belongs_to :shipping_address, :class_name => "Piggybak::Address" belongs_to :user accepts_nested_attributes_for :billing_address, :allow_destroy => true accepts_nested_attributes_for :shipping_address, :allow_destroy => true accepts_nested_attributes_for :shipments, :allow_destroy => true accepts_nested_attributes_for :line_items, :allow_destroy => true accepts_nested_attributes_for :payments end
An order has many line items, payments, shipments and credits. It belongs to [one] billing and [one] shipping address. It can accept nested attributes for the billing address, shipping address, multiple shipments, line items, and payments. It cannot destroy payments (they can only be marked as refunded). In terms of using ActiveRecord callbacks for an order save, this means that all the nested attributes will also be validated during the save. Validation fails if any nested model data is not valid.
Step #1: user enters data, and clicks submit
Step #2: before_validation
Using a before_validation ActiveRecord callback, a few things happen on the order:
- Some order defaults are set
- The order total is reset
- The order total due is reset
Step #3: validation
This happens without a callback. This method will execute validation on both order attributes (email, phone) and nested element attributes (address fields, shipment information, payment information, line_item information).
Payments have a special validation step here. A custom validation method on the payment attributes is performed to confirm validity of the credit card:
validates_each :payment_method_id do |record, attr, value|
if record.new_record?
credit_card = ActiveMerchant::Billing::CreditCard.new(record.credit_card)
if !credit_card.valid?
credit_card.errors.each do |key, value|
if value.any? && !["first_name", "last_name", "type"].include?(key)
record.errors.add key, value
end
end
end
end
end
This bit of code uses ActiveMerchant's functionality to avoid reproducing business logic for credit card validation. The errors are added on the payment attributes (e.g. card_number, verification_code, expiration date) and presented to the user.
Step #4: after_validation
Next, the after_validation callback is used to update totals. It does a few things here:
- Calculates shipping costs for new shipments only.
- Calculates tax charge on the order.
- Subtracts credits on the order, if they exist.
- Calculates total_due, to be used by payment
While these calculations could be performed before_validation, after_validation is a bit more performance-friendly since tax and shipping calculations could in theory be expensive (e.g. shipping calculations could require calling an external API for real-time shipping lookup). These calculations are saved until after the order is confirmed to be valid.
Step #5: before_save part 1
Next, a before_save callback handles payment (credit card) processing. This must happen after validation has passed, and it can not happen after the order has saved because the user must be notified if it fails. If any before_save method returns false, the entire transaction fails. So in this case, after all validation has passed, and before the order saves, the payment must process successfully.
Examples of failures here include:
- Credit card transaction denied for a number of reasons
- Payment gateway down
- Payment gateway API information incorrect
Step #6: before_save part 2
After the payment processes, another before_save method is called to update the status of the order based on the totals paid. I initially tried placing this in an after_save method, but you tend to experience infinite loops if you try to save inside and after_save callback :)
Step #7: Save
Finally, if everything's gone through, the order is saved.
Summary
As I mentioned above, the RailsAdmin controllers were not extended or overridden to handle backroom order processing. All of the order processing is represented in the Order model in these active record callbacks. This also allows for the frontend order processing controller to be fairly lightweight, which is a standard practice for writing clean MVC code.
Check out the full list of ActiveRecord callbacks here. And check out the Order model for Piggybak here.
Come Meet Me at the Worthing Thing, 26th Jan
I will be attending this month?s Worthing Thing (as I have been for a year now, but I wanted to draw some attention to the event)?a meet up of tech-minded indviduals, usually for a pint. It?s open to anybody involved with ?digital?; be that web development, ?social media?, content production?even printing.
This month will be slightly different as a company called Fresh Egg have offered to host at their offices (I don?t know if there?ll be drinks :() as an ?Annual General Meeting? of sorts. Even if you?ve never been before (and if you?re reading this, that?s likely true), you?re still invited to come talk tech with diverse but like-minded individuals including myself.
It will be held at 8pm on the 26th of January at the offices of Fresh Egg, 1?15 Buckingham Road, Worthing, BN11 1TH (England, BTW). Even you HTML5-hippies from Brighton are welcome :)
Hope to see you there.
In a previous post I talked about running cucumber using capybara-webkit. In a recent project using this setup I noticed that I couldn't use capybara in connection with launchy to open up a page in the browser for debugging tests. The "save and open page" step is one that I used a lot when I was developing locally. But now that I'm developing on a server, I don't have any way save the page or open it for review.
The solution I found to this comes in two parts. First, create a "take a snapshot" cucumber step that drops a snapshot of the HTML and a PNG of the page in a temp directory. Second, add that temp directory to dropbox so that it gets synced to my desktop automatically when it is created.
Wait, seriously? Dropbox?
Yes, absolutely. Dropbox.
I often develop inside of my dropbox folder because A) all my code is automatically backed up, even with versions and B) because it's really simple to sync my code to other computers. I'll admit that one problem I had early on was that log files were using an awful amount of bandwidth getting copied up constantly, but I solved this by adding the log directory to an exclusions list. I'll show you how to do that below.
Step 1: Create a "take a snapshot" step.
The first thing we need to do is setup our take a snapshot step. For our app, it made the most sense to put this in web_steps.rb but you can add it to any of your step_definitions files. The step looks like this:
Then /take a snapshot/ do
# save a html snapshot
html_snapshot = save_page
puts "Snapshot saved: n#{html_snapshot}"
# save a png snapshot
png_snapshot = html_snapshot.gsub(/html$/, "png")
page.driver.render png_snapshot
puts "Snapshot saved: n#{png_snapshot}"
end
The first line there is fairly simple. Capybara provides a save_page method by default which is going to save the page to tmp/capybara off the root of your app. The file will look something like this: capybara-20111228210921591550991.html. You can see the source code on the rdoc page for more information on how this works. If you want to customize the file name, you can do that by calling Capybara.save_page directly like this:
Capybara.save_page(body, 'my_custom_file.html')
This reveals what save_page is actually doing. Every cucumber step has available to it by default Capybara::Session. The source html of the current page is stored in Capybara::Session#body. The save_page method is just writing the source html to a file.
The next block of code saves the page as a PNG file. This comes from the capybara-webkit driver so this will only work if you are using that driver specifically. (You can explore the code on github to get more information, but basically it's calling the "Render" command on webkit and then storing the image as a PNG.)
All I'm doing here is changing out the html file extension for png so that the files will be easy to find. You can also pass width and height options if you'd like with a hash {:width => 1000, :height => 10}.
Step 2: Setup Dropbox.
I didn't know this until recently but you can run dropbox on a linux server without a UI. It is available as a binary for Ubuntu (.deb), Fedora (.rpm), Debian (.deb). You can also compile from source if you'd like. Since I'm doing my development on a server, however, I wanted a little bit more of an isolated installation and luckily Dropbox has the answer for me. It's called their command line installation and it works great. Here are the instructions from the web site:
For 32 bit:
cd ~ && wget -O - http://www.dropbox.com/download?plat=lnx.x86 | tar xzf -
or 64 bit:
cd ~ && wget -O - http://www.dropbox.com/download?plat=lnx.x86_64 | tar xzf -
You'll also need a small python script for working with the daemon. You can download it from the web site or from here.
The dropbox cli will walk you through a simple authentication process and then start downloading your dropbox folder in your ~/Dropbox directory when you run dropbox.py start for the first time. If your dropbox folder is like mine, this is going to download way more stuff than you'd probably want on your server so you'll need to add some exceptions. I created a folder in Dropbox for my screenshots called ~/Dropbox/cuke_snapshots and then excluded everything else. Here's how I did it with the dropbox.py file (I renamed dropbox.py to just dropbox for ease and clarity. It's also helpful to put it in a directory that's in your PATH):
cd ~/Dropbox
dropbox exlcude add Public Photos
That adds the Public and Photos folders to the exclusion list and Dropbox deletes them from the system for you. The nice thing is that you can continue to add folder names to the end of that command so you can get rid of stuff really quick. There are a bunch of options using the dropbox cli that make working with dropbox on the server very simple and flexible.
status get current status of the dropboxd
help provide help
puburl get public url of a file in your dropbox
stop stop dropboxd
running return whether dropbox is running
start start dropboxd
filestatus get current sync status of one or more files
ls list directory contents with current sync status
autostart automatically start dropbox at login
exclude ignores/excludes a directory from syncing
lansync enables or disables LAN sync
The last step is to create a symbolic link from your app into your dropbox folder. I did this with the following command:
mkdir -p ~/Dropbox/cuke_snapshots/my_app/capybara
cd ~/rails_apps/my_app/tmp
rm -rf capybara (if it already exists)
ln -s ~/Dropbox/cuke_snapshots/my_app/capybara
Of course there are many ways you could set this up, but this will get the job done.
Using "take a snapshot"
Once you have everything setup, all you need to do is call the cucumber step from within your scenarios. Here's a contrived example:
@javascript # may not be needed if everything is using webkit-capybara
Scenario: A shopper wants to checkout
When I go to the address step
And I fill in the address information
And I follow "Next"
Then I should be on the delivery step
And take a snapshot
When the scenario runs, it will drop the html and png file in your dropbox directory which will immediately be synced to your local machine. In my experience, by the time I open up the folder on my local machine, the file is there ready for inspection.








2 