<?xml version='1.0' encoding='UTF-8'?><?xml-stylesheet href="http://www.blogger.com/styles/atom.css" type="text/css"?><feed xmlns='http://www.w3.org/2005/Atom' xmlns:openSearch='http://a9.com/-/spec/opensearchrss/1.0/' xmlns:georss='http://www.georss.org/georss' xmlns:gd='http://schemas.google.com/g/2005' xmlns:thr='http://purl.org/syndication/thread/1.0'><id>tag:blogger.com,1999:blog-7997313029981170997</id><updated>2012-02-09T09:25:03.772-05:00</updated><category term='mobile'/><category term='Fedora'/><category term='postgres'/><category term='tools'/><category term='sysadmin'/><category term='pentaho'/><category term='community'/><category term='hosting'/><category term='analytics'/><category term='SELinux'/><category term='redhat'/><category term='audio'/><category term='travel'/><category term='css'/><category term='git'/><category term='tips'/><category term='nginx'/><category term='Spree'/><category term='cakephp'/><category term='email'/><category term='nosql'/><category term='social-networking'/><category term='unicode'/><category term='performance'/><category term='piggybak'/><category term='eye-candy'/><category term='reporting'/><category term='thrift'/><category term='facebook'/><category term='visualization'/><category term='openbsd'/><category term='mysql'/><category term='scalability'/><category term='riak'/><category term='security'/><category term='CentOS'/><category term='graphics'/><category term='cucumber'/><category term='rvm'/><category term='cloud'/><category term='django'/><category term='remote-work'/><category term='networking'/><category term='wordpress'/><category term='perlbrew'/><category term='optimization'/><category term='book review'/><category term='dropbox'/><category term='sinatra'/><category term='design'/><category term='mp3'/><category term='ruby-and-ruby-on-rails'/><category term='testing'/><category term='open-source'/><category term='json'/><category term='ruby'/><category term='yui'/><category term='virtualization'/><category term='COTS'/><category term='Camps'/><category term='javascript'/><category term='workflow'/><category term='sponsorship'/><category term='perl'/><category term='messaging'/><category term='environment'/><category term='gnu'/><category term='lua'/><category term='rpm'/><category term='browsers'/><category term='configuration-management'/><category term='python'/><category term='ecommerce'/><category term='Conference'/><category term='voldemort'/><category term='image'/><category term='jasper'/><category term='clients'/><category term='dbdpg'/><category term='Android'/><category term='database'/><category term='version-control'/><category term='linux'/><category term='USPS'/><category term='data-warehouse'/><category term='openafs'/><category term='cassandra'/><category term='mondaylinks'/><category term='php'/><category term='Debian'/><category term='monitoring'/><category term='liquid-galaxy'/><category term='audit'/><category term='Bucardo'/><category term='Java'/><category term='mongodb'/><category term='API'/><category term='seo'/><category term='SeniorNet'/><category term='company'/><category term='jquery'/><category term='jobs'/><category term='sql'/><category term='search'/><category term='Ubuntu'/><category term='Interchange'/><category term='caching'/><category term='ipv6'/><title type='text'>End Point Blog</title><subtitle type='html'>Ongoing observations by End Point people.</subtitle><link rel='http://schemas.google.com/g/2005#feed' type='application/atom+xml' href='http://blog.endpoint.com/feeds/posts/default'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/7997313029981170997/posts/default'/><link rel='alternate' type='text/html' href='http://blog.endpoint.com/'/><link rel='hub' href='http://pubsubhubbub.appspot.com/'/><link rel='next' type='application/atom+xml' href='http://www.blogger.com/feeds/7997313029981170997/posts/default?start-index=26&amp;max-results=25'/><author><name>Jon Jensen</name><uri>http://www.blogger.com/profile/18273388885281263476</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='25' src='http://bp3.blogger.com/_rFXHDrokbpE/SJHpPosaIQI/AAAAAAAAAAM/GnqeZuLItOA/S220/jon1.png'/></author><generator version='7.00' uri='http://www.blogger.com'>Blogger</generator><openSearch:totalResults>550</openSearch:totalResults><openSearch:startIndex>1</openSearch:startIndex><openSearch:itemsPerPage>25</openSearch:itemsPerPage><entry><id>tag:blogger.com,1999:blog-7997313029981170997.post-660973181045828665</id><published>2012-02-08T14:04:00.001-05:00</published><updated>2012-02-08T15:03:12.925-05:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='ruby'/><category scheme='http://www.blogger.com/atom/ns#' term='ecommerce'/><category scheme='http://www.blogger.com/atom/ns#' term='piggybak'/><category scheme='http://www.blogger.com/atom/ns#' term='ruby-and-ruby-on-rails'/><title type='text'>Download Functionality for Rails Ecommerce</title><content type='html'>&lt;p&gt;I recently had to build out downloadable product support for a client project running on &lt;a href="http://www.piggybak.org/"&gt;Piggybak (a Ruby on Rails Ecommerce engine)&lt;/a&gt; with extensive use of &lt;a href="https://github.com/sferik/rails_admin"&gt;RailsAdmin&lt;/a&gt;. 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 &lt;a href="https://github.com/thoughtbot/paperclip"&gt;paperclip&lt;/a&gt; for managing attachments, the general steps here would apply across languages and frameworks.&lt;/p&gt;

&lt;h3&gt;Data Migration&lt;/h3&gt;

&lt;p&gt;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:&lt;/p&gt;

&lt;pre&gt;
class CreateTracks &lt; ActiveRecord::Migration
  def change
    create_table :tracks do |t|
      # a bunch of fields specific to tracks

      t.boolean :is_downloadable, :nil =&gt; false, :default =&gt; false

      t.string :downloadable_file_name
      t.string :downloadable_content_type
      t.string :downloadable_file_size
      t.string :downloadable_updated_at
    end
  end
end
&lt;/pre&gt;

&lt;h3&gt;Class Definitions&lt;/h3&gt;

&lt;p&gt;Next, I update my class definition to make tracks sellable and hook in paperclip functionality:&lt;/p&gt;

&lt;pre&gt;
class Track &lt; ActiveRecord::Base
  acts_as_variant

  has_attached_file :downloadable,
                    :path =&gt; ":rails_root/downloads/:id/:basename.:extension",
                    :url =&gt; "downloads/:id/:basename.:extension"
end
&lt;/pre&gt;

&lt;p&gt;The important thing to note here is that the attached downloadable files &lt;b&gt;must not&lt;/b&gt; 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.&lt;/p&gt;

&lt;h3&gt;Shipping&lt;/h3&gt;

&lt;p&gt;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 &lt;a href="http://www.fakingfantastic.com/2010/09/20/concerning-yourself-with-active-support-concern/"&gt;ActiveSupport::Concern&lt;/a&gt; to check whether or not an order is downloadable, with the following instance method:&lt;/p&gt;

&lt;pre&gt;
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)
&lt;/pre&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;div style="text-align:center;"&gt;
&lt;img border="0" height="213" width="400" src="http://1.bp.blogspot.com/-MKe5tZgjhG0/TzBAlfChtDI/AAAAAAAAE4A/GEE8Ah2SVLg/s400/free_shipping.png" /&gt;&lt;/div&gt;


&lt;h3&gt;Forcing Log In&lt;/h3&gt;

&lt;p&gt;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:&lt;/p&gt;

&lt;pre&gt;
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)
&lt;/pre&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;div class="separator" style="clear: both; text-align: center;"&gt;
&lt;img border="0" height="177" width="400" src="http://1.bp.blogspot.com/-iQWvSTEi6tg/TzBBIS7Fy_I/AAAAAAAAE4M/8lxuphc1Ufg/s400/force_login.png" /&gt;&lt;/div&gt;



&lt;h3&gt;Download List Page&lt;/h3&gt;

&lt;p&gt;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:&lt;/p&gt;

&lt;div class="separator" style="clear: both; text-align: center;"&gt;
&lt;a href="http://2.bp.blogspot.com/-5LYjF5Og6Ag/TzBBgDGXRnI/AAAAAAAAE4Y/zaODCCorlWQ/s1600/download_list.png" imageanchor="1" style="margin-left:1em; margin-right:1em"&gt;&lt;img border="0" height="380" width="400" src="http://2.bp.blogspot.com/-5LYjF5Og6Ag/TzBBgDGXRnI/AAAAAAAAE4Y/zaODCCorlWQ/s400/download_list.png" /&gt;&lt;/a&gt;&lt;/div&gt;

&lt;p&gt;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:&lt;/p&gt;

&lt;pre&gt;
class User &lt; ActiveRecord::Base
  ...
  def downloads_by_order
    self.piggybak_orders.inject([]) do |arr, order|
      downloads = []
      order.line_items.each do |line_item|
        downloads &lt;&lt; line_item.variant.item if line_item.variant.item.is_downloadable?
      end

      arr &lt;&lt; {
          :order =&gt; order,
          :downloads =&gt; downloads
      } if downloads.any?
      arr
    end
  end
end
&lt;/pre&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h3&gt;Sending Files&lt;/h3&gt;

&lt;p&gt;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:&lt;/p&gt;

&lt;pre&gt;
class DownloadsController &lt; 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 =&gt; "You do not have access to this content.")
    end
  end
end
&lt;/pre&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h3&gt;Conclusion&lt;/h3&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;If you are interested in this project, check out these related articles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="http://blog.endpoint.com/2012/01/piggybak-mountable-ecommerce-ruby-on.html"&gt;Introducing Piggybak: A Mountable Ruby on Rails Ecommerce Engine&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="http://blog.endpoint.com/2012/01/activerecord-callbacks-ecommerce-order.html"&gt;ActiveRecord Callbacks for Order Processing in Ecommerce Applications&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="http://blog.endpoint.com/2012/01/import-railsadmin.html"&gt;Importing into RailsAdmin: Part 1&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="http://blog.endpoint.com/2012/02/railsadmin-import-part-2.html"&gt;Importing into RailsAdmin: Part 2&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Or read more about End Point's &lt;a href="http://www.endpoint.com/services"&gt;web development and consulting services&lt;/a&gt;!&lt;/p&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/7997313029981170997-660973181045828665?l=blog.endpoint.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://blog.endpoint.com/feeds/660973181045828665/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=7997313029981170997&amp;postID=660973181045828665' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/7997313029981170997/posts/default/660973181045828665'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/7997313029981170997/posts/default/660973181045828665'/><link rel='alternate' type='text/html' href='http://blog.endpoint.com/2012/02/download-functionality-rails-ecommerce.html' title='Download Functionality for Rails Ecommerce'/><author><name>Steph Skardal</name><uri>http://www.blogger.com/profile/11721782860178431713</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media='http://search.yahoo.com/mrss/' url='http://1.bp.blogspot.com/-MKe5tZgjhG0/TzBAlfChtDI/AAAAAAAAE4A/GEE8Ah2SVLg/s72-c/free_shipping.png' height='72' width='72'/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-7997313029981170997.post-214974046530672513</id><published>2012-02-01T16:52:00.002-05:00</published><updated>2012-02-03T15:25:56.676-05:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='piggybak'/><category scheme='http://www.blogger.com/atom/ns#' term='database'/><category scheme='http://www.blogger.com/atom/ns#' term='ruby-and-ruby-on-rails'/><title type='text'>RailsAdmin Import: Part 2</title><content type='html'>&lt;p&gt;I recently wrote about &lt;a href="http://blog.endpoint.com/2012/01/import-railsadmin.html"&gt;importing data in RailsAdmin&lt;/a&gt;. &lt;a href="https://github.com/sferik/rails_admin"&gt;RailsAdmin&lt;/a&gt; is a Rails engine that provides a nice admin interface for managing your data, which comes packed with configuration options.&lt;/p&gt;

&lt;p&gt;In a recent Ruby on Rails ecommerce project, I've been using RailsAdmin, &lt;a href="http://www.piggybak.org/"&gt;Piggybak (a Rails ecommerce gem supported by End Point)&lt;/a&gt;, 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:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;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.&lt;/li&gt;
&lt;li&gt;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.&lt;/li&gt;
&lt;li&gt;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.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The client preferred option #3. Using &lt;a href="https://github.com/sferik/rails_admin/wiki/Custom-action"&gt;a quick script for generating custom actions for RailsAdmin&lt;/a&gt;, 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.&lt;/p&gt;

&lt;img border="0" width="750" src="http://3.bp.blogspot.com/-d_6dK1puG6o/Tymx2ELzCoI/AAAAAAAAE3o/UIKq7I4-LXI/s1600/import.png" /&gt;

&lt;h3&gt;ActiveSupport::Concern&lt;/h3&gt;

&lt;p&gt;Using &lt;a href="http://www.fakingfantastic.com/2010/09/20/concerning-yourself-with-active-support-concern/"&gt;ActiveSupport::Concern&lt;/a&gt;, the rails_admin_import gem extends ActiveRecord::Base to add the following class methods:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;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.&lt;/li&gt;
&lt;li&gt;belongs_to_fields: Returns an array of fields with belongs_to relationships to other models.&lt;/li&gt;
&lt;li&gt;many_to_many_fields: Returns an array of fields with has_and_belongs_to_many relationships to other models.&lt;/li&gt;
&lt;li&gt;file_fields: Returns an array of fields that represent data for &lt;a href="https://github.com/thoughtbot/paperclip"&gt;Paperclip&lt;/a&gt; attached files.&lt;/li&gt;
&lt;li&gt;run_import: Method for running the actual import, receives request params.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And the following instance methods:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;import_files: sets attached files for object&lt;/li&gt;
&lt;li&gt;import_belongs_to_data: sets belongs_to associated data for object&lt;/li&gt;
&lt;li&gt;import_many_to_many_data: sets many_to_many associated data for object&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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 &lt;a href="http://api.rubyonrails.org/classes/ActiveRecord/Base.html"&gt;ActiveRecord&lt;/a&gt; save method is called on the object, so the existing validation of objects during each save applies.&lt;/p&gt;

&lt;h3&gt;Working with Associated Data&lt;/h3&gt;

&lt;p&gt;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?&lt;/p&gt;

&lt;img border="0" height="167" width="400" src="http://4.bp.blogspot.com/-NlWxG0Y0Fq0/Tymx2aFPAHI/AAAAAAAAE34/eU7XIzvS4Cs/s400/mapping.png" /&gt;

&lt;p&gt;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:&lt;/p&gt;

&lt;table width="100%" cellpadding="0" cellspacing="0" style="border:1px solid #999;"&gt;
&lt;tr&gt;
&lt;td&gt;name&lt;/td&gt;
&lt;td&gt;email&lt;/td&gt;
&lt;td&gt;favorite_color&lt;/td&gt;
&lt;td&gt;state&lt;/td&gt;
&lt;td&gt;country&lt;/td&gt;
&lt;td&gt;role&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Steph Skardal&lt;/td&gt;
&lt;td&gt;steph@endpoint.com&lt;/td&gt;
&lt;td&gt;blue&lt;/td&gt;
&lt;td&gt;CO&lt;/td&gt;
&lt;td&gt;United States&lt;/td&gt;
&lt;td&gt;admin&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Aleks Skardal&lt;/td&gt;
&lt;td&gt;aleksskardal@gmail.com&lt;/td&gt;
&lt;td&gt;green&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;Norway&lt;/td&gt;
&lt;td&gt;user&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Roger Skardal&lt;/td&gt;
&lt;td&gt;roger@gmail.com&lt;/td&gt;
&lt;td&gt;tennis ball yellow&lt;/td&gt;
&lt;td&gt;UT&lt;/td&gt;
&lt;td&gt;United States&lt;/td&gt;
&lt;td&gt;dog&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Milton Skardal&lt;/td&gt;
&lt;td&gt;milton@gmail.com&lt;/td&gt;
&lt;td&gt;kibble brown&lt;/td&gt;
&lt;td&gt;UT&lt;/td&gt;
&lt;td&gt;United States&lt;/td&gt;
&lt;td&gt;dog&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;

&lt;h3&gt;Many to Many Relationships&lt;/h3&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h3&gt;Import of File Fields&lt;/h3&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;pre&gt;
self.class.file_fields.each do |key|
  if map[key] &amp;&amp; !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 &lt;&lt; open(row[map[key]]).read }
      self.send("#{key}=", File.open("#{Rails.root}/tmp/uploads/#{self.permalink}.#{format}"))
    rescue Exception =&gt; e
      self.errors.add(:base, "Import error: #{e.inspect}")
    end
  end
end
&lt;/pre&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h3&gt;Configuration: Display&lt;/h3&gt;

&lt;p&gt;Following RailsAdmin's example for setting configurations, I've added the ability to allow the import display to be set for each model.&lt;/p&gt;

&lt;pre&gt;
config.model User do
  label :name
end
&lt;/pre&gt;

&lt;p&gt;The above configuration will yield success and error messages with the user.name, e.g.:&lt;/p&gt;

&lt;img border="0" height="244" width="400" src="http://1.bp.blogspot.com/-7_kFir9sHfE/TymugH6t4cI/AAAAAAAAE3c/C2O_--Y_Wic/s400/errors.png" /&gt;

&lt;h3&gt;Configuration: Excluded Fields&lt;/h3&gt;

&lt;p&gt;In addition to allowing a configurable display option, I've added the configuration for excluding fields.&lt;/p&gt;

&lt;pre&gt;
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
&lt;/pre&gt;

&lt;p&gt;The above configuration will exclude the specified fields during the import, and they will not display on the import page.&lt;/p&gt;

&lt;h3&gt;Configuration: Additional Fields and Additional Processing&lt;/h3&gt;

&lt;p&gt;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 &lt;b&gt;before_import_save&lt;/b&gt; that accepts the row of CSV data and map of CSV keys to perform additional tasks. For example:&lt;/p&gt;

&lt;pre&gt;
def before_import_save(row, map)
  self.created_nested_items(row, map)
end
&lt;/pre&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Fields for additional nested data can be defined with the extra_fields configuration, and are shown on the import page.&lt;/p&gt;

&lt;pre&gt;
config.model User do
  extra_fields do
    [:field1, :field2, :field3, :field4]
  end
end
&lt;/pre&gt;

&lt;h3&gt;Hooking into RailsAdmin&lt;/h3&gt;

&lt;p&gt;As I mentioned above, I used a script to generate this Engine. Using RailsAdmin configurable actions, import must be added as an action:&lt;/p&gt;

&lt;pre&gt;
config.actions do
  dashboard
  index
  ...
  import
end
&lt;/pre&gt;

&lt;p&gt;And &lt;a href="https://github.com/ryanb/cancan"&gt;CanCan&lt;/a&gt; settings must be updated to allow for import if applicable, e.g.:&lt;/p&gt;
&lt;pre&gt;
cannot :import, :all
can :import, User
&lt;/pre&gt;

&lt;h3&gt;Conclusion&lt;/h3&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Feel free to review or check out the code &lt;a href="https://github.com/stephskardal/rails_admin_import"&gt;here&lt;/a&gt;, or read more about End Point's services &lt;a href="http://www.endpoint.com/services"&gt;here&lt;/a&gt;.&lt;/p&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/7997313029981170997-214974046530672513?l=blog.endpoint.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://blog.endpoint.com/feeds/214974046530672513/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=7997313029981170997&amp;postID=214974046530672513' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/7997313029981170997/posts/default/214974046530672513'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/7997313029981170997/posts/default/214974046530672513'/><link rel='alternate' type='text/html' href='http://blog.endpoint.com/2012/02/railsadmin-import-part-2.html' title='RailsAdmin Import: Part 2'/><author><name>Steph Skardal</name><uri>http://www.blogger.com/profile/11721782860178431713</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media='http://search.yahoo.com/mrss/' url='http://3.bp.blogspot.com/-d_6dK1puG6o/Tymx2ELzCoI/AAAAAAAAE3o/UIKq7I4-LXI/s72-c/import.png' height='72' width='72'/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-7997313029981170997.post-7734681975994677033</id><published>2012-01-30T22:55:00.001-05:00</published><updated>2012-01-30T22:55:59.092-05:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='security'/><category scheme='http://www.blogger.com/atom/ns#' term='perl'/><category scheme='http://www.blogger.com/atom/ns#' term='postgres'/><category scheme='http://www.blogger.com/atom/ns#' term='database'/><category scheme='http://www.blogger.com/atom/ns#' term='audit'/><title type='text'>Protecting and auditing your secure PostgreSQL data</title><content type='html'>&lt;a href="http://4.bp.blogspot.com/-1R42QgfZvFs/TycvCoG8BtI/AAAAAAAAAPU/xJIL640LkBk/s1600/EIB.png"&gt;&lt;img style="float:right; margin:0 0 10px 10px;cursor:pointer; cursor:hand;width: 310px; height: 320px;" src="http://4.bp.blogspot.com/-1R42QgfZvFs/TycvCoG8BtI/AAAAAAAAAPU/xJIL640LkBk/s320/EIB.png" border="0" alt=""id="BLOGGER_PHOTO_ID_5703579175260784338" /&gt;&lt;/a&gt;&lt;style&gt;span{font-family:sans-serif;}span.p{color:red;}span.d{color:gray;}span.c{color:green;font-family:monospace;}span.o{font-family:monospace;color:#0022cc;}span.oo{font-family:monospace;font-weight:bolder;}&lt;/style&gt;&lt;p&gt;PostgreSQL functions can be written in &lt;a href="http://www.postgresql.org/docs/9.1/static/xplang.html"&gt;many languages&lt;/a&gt;. 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 &lt;a href="http://www.postgresql.org/docs/9.1/static/plpgsql.html"&gt;PL/pgSQL&lt;/a&gt; and and &lt;a href="http://www.postgresql.org/docs/9.1/static/plperl.html"&gt;PL/Perl&lt;/a&gt;. 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.&lt;/p&gt;&lt;p&gt;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.&lt;/p&gt;&lt;p&gt;However, there are times when this effect can be very useful. For example, in a &lt;a href="http://postgresql.1045698.n5.nabble.com/Logging-access-to-data-in-database-table-td5430079.html"&gt; recent thread&lt;/a&gt; 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:&lt;/p&gt;&lt;pre&gt;&lt;span class="o"&gt;
BEGIN;

SELECT weapon_details('BFG 9000'); -- also inserts to an audit table

ROLLBACK;                          -- inserts to the audit table are now gone!
&lt;/span&gt;&lt;/pre&gt;&lt;p&gt;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.&lt;/p&gt;&lt;p&gt;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 &lt;a href="http://www.postgresql.org/docs/9.1/static/app-psql.html"&gt;psql&lt;/a&gt; access and needs to be able to run ad hoc SQL queries, as well as be able to BEGIN, ROLLBACK, COMMIT, etc.&lt;/p&gt;&lt;p&gt;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:&lt;/p&gt;&lt;pre&gt;&lt;span class="o"&gt;
&lt;span class="p"&gt;postgres=&gt; &lt;/span&gt;CREATE USER alice;
&lt;span class="d"&gt;CREATE ROLE&lt;/span&gt;
&lt;/span&gt;&lt;/pre&gt;&lt;p&gt;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 &lt;a href="http://www.postgresql.org/docs/9.1/static/datatype-enum.html"&gt;ENUM feature&lt;/a&gt; of PostgreSQL:&lt;/p&gt;&lt;pre&gt;&lt;span class="o"&gt;
&lt;span class="p"&gt;postgres=&gt; &lt;/span&gt;CREATE TYPE classification AS ENUM (
 'unclassified',
 'restricted',
 'confidential',
 'secret',
 'top secret'
);
&lt;span class="d"&gt;CREATE TYPE&lt;/span&gt;
&lt;/span&gt;&lt;/pre&gt;&lt;p&gt;Next, as a superuser, we create the table containing sensitive information, and populate it:&lt;/p&gt;&lt;pre&gt;&lt;span class="o"&gt;
&lt;span class="p"&gt;postgres=&gt; &lt;/span&gt;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'
);
&lt;span class="d"&gt;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&lt;/span&gt;

&lt;span class="p"&gt;postgres=&gt; &lt;/span&gt;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');
&lt;span class="d"&gt;INSERT 0 7&lt;/span&gt;
&lt;/span&gt;&lt;/pre&gt;&lt;p&gt;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:&lt;/p&gt;&lt;pre&gt;&lt;span class="o"&gt;
&lt;span class="p"&gt;postgres=&gt; &lt;/span&gt;\dp weapon
&lt;span class="c"&gt;                           Access privileges
 Schema |  Name  | Type  | Access privileges | Column access privileges 
--------+--------+-------+-------------------+--------------------------
 public | weapon | table |                   | &lt;/span&gt;

&lt;span class="p"&gt;postgres=&gt; &lt;/span&gt;REVOKE ALL ON TABLE weapon FROM public;
&lt;span class="d"&gt;REVOKE&lt;/span&gt;

&lt;span class="p"&gt;postgres=&gt; &lt;/span&gt;\dp weapon
&lt;span class="c"&gt;
                               Access privileges
 Schema |  Name  | Type  |     Access privileges     | Column access privileges 
--------+--------+-------+---------------------------+--------------------------
 public | weapon | table | postgres=arwdDxt/postgres | &lt;/span&gt;
&lt;/span&gt;&lt;/pre&gt;&lt;p&gt;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:&lt;/p&gt;&lt;pre&gt;&lt;span class="o"&gt;
&lt;span class="p"&gt;postgres=&gt; &lt;/span&gt;\c postgres alice
&lt;span class="c"&gt;You are now connected to database "postgres" as user "alice".&lt;/span&gt;
&lt;span class="p"&gt;postgres=&gt; &lt;/span&gt;postgres=&gt; SELECT * FROM weapon;
&lt;span class="c"&gt;ERROR:  permission denied for relation weapon&lt;/span&gt;
&lt;span class="p"&gt;postgres=&gt; &lt;/span&gt;postgres=&gt; UPDATE weapon SET id = id;
&lt;span class="c"&gt;ERROR:  permission denied for relation weapon&lt;/span&gt;
&lt;/span&gt;&lt;/pre&gt;&lt;p&gt;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:&lt;/p&gt;&lt;pre&gt;&lt;span class="o"&gt;
&lt;span class="p"&gt;postgres=&gt; &lt;/span&gt;CREATE LANGUAGE plperlu;
&lt;span class="d"&gt;CREATE LANGUAGE&lt;/span&gt;

&lt;span class="p"&gt;postgres=&gt; &lt;/span&gt;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 &lt;= $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-&gt;{rows}[0];
my $allowed = delete $row-&gt;{allowed};

## Did we find anything? If not, simply return undef
if (! $rv-&gt;{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$;
&lt;span class="d"&gt;CREATE FUNCTION&lt;/span&gt;
&lt;/span&gt;&lt;/pre&gt;&lt;p&gt;The above should be fairly self-explanatory. We are using PL/Perl's &lt;a href="http://www.postgresql.org/docs/9.1/static/plperl-builtins.html"&gt;built-in database access functions&lt;/a&gt;, such as spi_prepare, to do the actual querying. Let's confirm that this works as it should for Alice:&lt;/p&gt;&lt;pre&gt;&lt;span class="o"&gt;
&lt;span class="p"&gt;postgres=&gt; &lt;/span&gt;\c postgres alice
&lt;span class="c"&gt;You are now connected to database "postgres" as user "alice".&lt;/span&gt;

&lt;span class="p"&gt;postgres=&gt; &lt;/span&gt;SELECT * FROM weapon_details('crowbar');
&lt;span class="c"&gt;  name   | cost |  description  
---------+------+---------------
 Crowbar | 10   | a fine weapon&lt;/span&gt;
&lt;span class="d"&gt;(1 row)&lt;/span&gt;

&lt;span class="p"&gt;postgres=&gt; &lt;/span&gt;SELECT * FROM weapon_details('anvil');
&lt;span class="c"&gt; name | cost | description 
------+------+-------------&lt;/span&gt;
&lt;span class="d"&gt;(0 rows)&lt;/span&gt;

&lt;span class="p"&gt;postgres=&gt; &lt;/span&gt;SELECT * FROM weapon_details('pulse rifle');
&lt;span class="c"&gt;ERROR:  Sorry, you are not allowed to view information on that weapon!
CONTEXT:  PL/Perl function "weapon_details"&lt;/span&gt;
&lt;/span&gt;&lt;/pre&gt;&lt;p&gt;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:&lt;/p&gt;&lt;pre&gt;&lt;span class="o"&gt;
&lt;span class="p"&gt;postgres=&gt; &lt;/span&gt;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()
);
&lt;span class="d"&gt;CREATE TABLE&lt;/span&gt;
&lt;/span&gt;&lt;/pre&gt;&lt;p&gt;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:&lt;/p&gt;&lt;pre&gt;&lt;span class="o"&gt;
&lt;span class="p"&gt;postgres=&gt; &lt;/span&gt;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 &lt;= $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-&gt;{rows}[0];
my $allowed = delete $row-&gt;{allowed};

&lt;span class="oo"&gt;
## Log this request
$SQL = 'INSERT INTO data_audit(tablename,arguments,results,status)
  VALUES ($1,$2,$3,$4)';
my $status = $rv-&gt;{rows}[0] ? $allowed ? 'normal' : 'forbidden' : 'na';
$sth = spi_prepare($SQL, 'TEXT', 'TEXT', 'INTEGER', 'TEXT');
spi_exec_prepared($sth, 'weapon', $name, $rv-&gt;{processed}, $status);
&lt;/span&gt;

## Did we find anything? If not, simply return undef
if (! $rv-&gt;{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$;
&lt;/span&gt;&lt;/pre&gt;&lt;p&gt;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:&lt;/p&gt;&lt;pre&gt;&lt;span class="o"&gt;
&lt;span class="p"&gt;postgres=&gt; &lt;/span&gt;\c postgres alice
&lt;span class="c"&gt;You are now connected to database "postgres" as user "alice".&lt;/span&gt;

&lt;span class="p"&gt;postgres=&gt; &lt;/span&gt;SELECT * FROM weapon_details('crowbar');
&lt;span class="c"&gt;  name   | cost |  description  
---------+------+---------------
 Crowbar | 10   | a fine weapon
(1 row)&lt;/span&gt;

&lt;span class="p"&gt;postgres=&gt; &lt;/span&gt;SELECT * FROM weapon_details('pulse rifle');
&lt;span class="c"&gt;ERROR:  Sorry, you are not allowed to view information on that weapon!
CONTEXT:  PL/Perl function "weapon_details"&lt;/span&gt;

&lt;span class="p"&gt;postgres=&gt; &lt;/span&gt;BEGIN;
&lt;span class="d"&gt;BEGIN&lt;/span&gt;
&lt;span class="p"&gt;postgres=&gt; &lt;/span&gt;SELECT * FROM weapon_details('m9');
&lt;span class="c"&gt; name | cost |  description  
------+------+---------------
 M9   | 200  | a fine weapon&lt;/span&gt;
&lt;span class="d"&gt;(1 row)&lt;/span&gt;
&lt;span class="p"&gt;postgres=&gt; &lt;/span&gt;ROLLBACK;
&lt;span class="d"&gt;ROLLBACK&lt;/span&gt;

&lt;span class="p"&gt;postgres=&gt; &lt;/span&gt;\c postgres postgres
&lt;span class="c"&gt;You are now connected to database "postgres" as user "postgres".&lt;/span&gt;
&lt;span class="p"&gt;postgres=&gt; &lt;/span&gt;SELECT * FROM data_audit \x \g
&lt;span class="d"&gt;Expanded display is on.&lt;/span&gt;
&lt;span class="c"&gt;-[ 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&lt;/span&gt;
&lt;/span&gt;&lt;/pre&gt;&lt;p&gt;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 &lt;a href="http://www.postgresql.org/docs/9.1/static/dblink.html"&gt;the dblink program&lt;/a&gt; and &lt;a href="http://www.postgresql.org/docs/9.1/static/plperl.html"&gt;the PL/PerlU language&lt;/a&gt;. 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:&lt;/p&gt;&lt;pre&gt;&lt;span class="o"&gt;
&lt;span class="p"&gt;postgres=&gt; &lt;/span&gt;CREATE OR REPLACE FUNCTION weapon_details(TEXT)
RETURNS TABLE (name TEXT, cost TEXT, description TEXT)
LANGUAGE plperlu
SECURITY DEFINER
&lt;span class="oo"&gt;VOLATILE&lt;/span&gt;
AS $bc$

use strict;
use warnings;
&lt;span class="oo"&gt;use DBI;&lt;/span&gt;

## 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 &lt;= $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-&gt;{rows}[0];
my $allowed = defined $row ? delete $row-&gt;{allowed} : 1;

## Log this request
&lt;span class="oo"&gt;$SQL = 'INSERT INTO data_audit(username,tablename,arguments,results,status)
  VALUES (?,?,?,?,?)';
my $status = $rv-&gt;{rows}[0] ? $allowed ? 'normal' : 'forbidden' : 'na';
my $dbh = DBI-&gt;connect('dbi:Pg:service=auditor', '', '',
  {AutoCommit=&gt;0, RaiseError=&gt;1, PrintError=&gt;0});
$sth = $dbh-&gt;prepare($SQL);
my $user = spi_exec_query('SELECT session_user')-&gt;{rows}[0]{session_user};
$sth-&gt;execute($user, 'weapon', $name, $rv-&gt;{processed}, $status);
$dbh-&gt;commit();&lt;/span&gt;

## Did we find anything? If not, simply return undef
if (! $rv-&gt;{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$;
&lt;span class="d"&gt;CREATE FUNCTION&lt;/span&gt;
&lt;/span&gt;&lt;/pre&gt;&lt;p&gt;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 &lt;a href="http://www.postgresql.org/docs/9.1/static/libpq-pgservice.html"&gt;a Postgres service file&lt;/a&gt; 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.&lt;/p&gt;&lt;p&gt;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:&lt;/p&gt;&lt;pre&gt;&lt;span class="o"&gt;
&lt;span class="p"&gt;postgres=&gt; &lt;/span&gt;\c postgres postgres
&lt;span class="c"&gt;You are now connected to database "postgres" as user "postgres".
&lt;span class="d"&gt;Expanded display is on.&lt;/span&gt;
-[ 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&lt;/span&gt;
&lt;/span&gt;&lt;/pre&gt;&lt;p&gt;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 &lt;a href="http://www.postgresql.org/docs/9.1/static/plperl-global.html"&gt;%_SHARED hash available to PL/Perl and Pl/PerlU&lt;/a&gt;. 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:&lt;/p&gt;&lt;pre&gt;&lt;span class="o"&gt;
&lt;span class="p"&gt;postgres=&gt; &lt;/span&gt; \c postgres alice
&lt;span class="c"&gt;You are now connected to database "postgres" as user "alice".&lt;/span&gt;

&lt;span class="p"&gt;postgres=&gt; &lt;/span&gt;CREATE FUNCTION showplatform()
RETURNS TEXT
LANGUAGE plperlu
AS $bc$
  return $^O;
$bc$;
&lt;span class="c"&gt;ERROR:  permission denied for language plperlu&lt;/span&gt;
&lt;/span&gt;&lt;/pre&gt;&lt;p&gt;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!&lt;/p&gt;&lt;p&gt;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. :)&lt;/p&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/7997313029981170997-7734681975994677033?l=blog.endpoint.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://blog.endpoint.com/feeds/7734681975994677033/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=7997313029981170997&amp;postID=7734681975994677033' title='2 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/7997313029981170997/posts/default/7734681975994677033'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/7997313029981170997/posts/default/7734681975994677033'/><link rel='alternate' type='text/html' href='http://blog.endpoint.com/2012/01/protecting-auditing-postgresql-data.html' title='Protecting and auditing your secure PostgreSQL data'/><author><name>Greg Sabino Mullane</name><uri>http://www.blogger.com/profile/01284672712225991979</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media='http://search.yahoo.com/mrss/' url='http://4.bp.blogspot.com/-1R42QgfZvFs/TycvCoG8BtI/AAAAAAAAAPU/xJIL640LkBk/s72-c/EIB.png' height='72' width='72'/><thr:total>2</thr:total></entry><entry><id>tag:blogger.com,1999:blog-7997313029981170997.post-3633286005608369093</id><published>2012-01-27T20:10:00.008-05:00</published><updated>2012-01-27T21:28:08.181-05:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='redhat'/><category scheme='http://www.blogger.com/atom/ns#' term='CentOS'/><category scheme='http://www.blogger.com/atom/ns#' term='linux'/><category scheme='http://www.blogger.com/atom/ns#' term='Fedora'/><category scheme='http://www.blogger.com/atom/ns#' term='sysadmin'/><category scheme='http://www.blogger.com/atom/ns#' term='Debian'/><category scheme='http://www.blogger.com/atom/ns#' term='security'/><category scheme='http://www.blogger.com/atom/ns#' term='Ubuntu'/><title type='text'>Linux unshare -m for per-process private filesystem mount points</title><content type='html'>&lt;h2&gt;Private mount points with unshare&lt;/h2&gt;

&lt;p&gt;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 &lt;strong&gt;&lt;tt&gt;unshare()&lt;/tt&gt; function&lt;/strong&gt;, which the &lt;tt&gt;unshare(2)&lt;/tt&gt; 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 &lt;tt&gt;fork(2)&lt;/tt&gt; or &lt;tt&gt;vfork(2)&lt;/tt&gt;”.

&lt;p&gt;I’m going to talk here about one option to unshare: &lt;strong&gt;per-process private filesystem mount points&lt;/strong&gt;, also described as mount namespaces. This Linux kernel feature has been around for a few years and is easily accessible in the userland command &lt;tt&gt;unshare(1)&lt;/tt&gt; 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).&lt;/p&gt;

&lt;p&gt;Running &lt;tt&gt;`unshare -m`&lt;/tt&gt; 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.&lt;/p&gt;

&lt;p&gt;Yes, completely private mount points for each process. Isn’t that interesting and strange?&lt;/p&gt;

&lt;h2&gt;A demonstration&lt;/h2&gt;

&lt;p&gt;Here’s a demonstration on an Ubuntu 11.04 system. In one terminal:&lt;/p&gt;

&lt;pre&gt;
% 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% /
&lt;/pre&gt;

&lt;p&gt;There’s no system-wide sign of &lt;tt&gt;/tmp/tmp.*&lt;/tt&gt; there thanks to &lt;tt&gt;mount -n&lt;/tt&gt; which hides it. But it can be seen process-private here:&lt;/p&gt;

&lt;pre&gt;
# 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
&lt;/pre&gt;

&lt;p&gt;Afterward, in another terminal, and thus a separate process with no visibility into the above-shown terminal process’s private mount points:&lt;/p&gt;

&lt;pre&gt;
% 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 ../
&lt;/pre&gt;

&lt;p&gt;It’s all secret!&lt;/p&gt;

&lt;h2&gt;Use cases&lt;/h2&gt;

&lt;p&gt;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. &lt;tt&gt;lsof&lt;/tt&gt;.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;&lt;tt&gt;/etc/mtab&lt;/tt&gt; vs. &lt;tt&gt;/proc/mounts&lt;/tt&gt;&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;First, the classic Unix &lt;strong&gt;&lt;tt&gt;/etc/mtab&lt;/tt&gt;&lt;/strong&gt;, 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 &lt;tt&gt;/etc/mtab&lt;/tt&gt; but the sizes are determined dynamically and differ based on process’s view into the mount points themselves. The &lt;tt&gt;`mount -n`&lt;/tt&gt; option tells mount to not put the new mount point into &lt;tt&gt;/etc/mtab&lt;/tt&gt;. And this is what the &lt;tt&gt;df(1)&lt;/tt&gt; command refers to. How repulsive that a file in the normally read-only &lt;tt&gt;/etc&lt;/tt&gt; is written to so nonchalantly!&lt;/p&gt;

&lt;p&gt;Second, the Linux-specific &lt;strong&gt;&lt;tt&gt;/proc/mounts&lt;/tt&gt;&lt;/strong&gt;, which is real-time, exact, and accurate, and can appear differently to each process. The mount invocation can’t hide anything from &lt;tt&gt;/proc/mounts&lt;/tt&gt;. This is what you would think is the only place to look for mounts, but &lt;tt&gt;/etc/mtab&lt;/tt&gt; is still used some places.&lt;/p&gt;

&lt;p&gt;Ubuntu 11.04 still has both, with a separate &lt;tt&gt;/etc/mtab&lt;/tt&gt;. Fedora 16 has done away with &lt;tt&gt;/etc/mtab&lt;/tt&gt; entirely and made it merely a symlink to &lt;tt&gt;/proc/mounts&lt;/tt&gt;, which makes sense, but that is a newer convention and leads to the surprising difference here.&lt;/p&gt;

&lt;h2&gt;Linux distributions and unshare&lt;/h2&gt;

&lt;p&gt;The &lt;tt&gt;unshare&lt;/tt&gt; userland command in &lt;tt&gt;util-linux(-ng)&lt;/tt&gt; comes with RHEL 6, Debian 6, Ubuntu 11.04, and Fedora 16, but &lt;em&gt;not&lt;/em&gt; 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 &lt;tt&gt;unshare(1)&lt;/tt&gt; command and peacefully coexists with the older stock RHEL 5 util-linux. It’s called &lt;tt&gt;util-linux-unshare&lt;/tt&gt; and here are the RPM downloads for RHEL 5:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;x86_64: &lt;a href="https://packages.endpoint.com/rhel/5/os/x86_64/util-linux-unshare-2.20.1-3.ep.x86_64.rpm"&gt;util-linux-unshare-2.20.1-3.ep.x86_64.rpm&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;i386: &lt;a href="https://packages.endpoint.com/rhel/5/os/i386/util-linux-unshare-2.20.1-3.ep.i386.rpm"&gt;util-linux-unshare-2.20.1-3.ep.i386.rpm&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;SRPM: &lt;a href="https://packages.endpoint.com/rhel/5/os/SRPMS/util-linux-unshare-2.20.1-3.ep.src.rpm"&gt;util-linux-unshare-2.20.1-3.ep.src.rpm&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I hope you’ve found this as interesting as I did!&lt;/p&gt;

&lt;h2&gt;Further reading&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Karel Zak is the util-linux maintainer and a Red Hat employee; see his &lt;a href="http://karelzak.blogspot.com/2009/12/unshare1.html"&gt;detailed blog post about the unshare command&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="http://linux.die.net/man/2/unshare"&gt;unshare(2)&lt;/a&gt; function man page&lt;/li&gt;
&lt;li&gt;&lt;a href="http://linux.die.net/man/1/unshare"&gt;unshare(1)&lt;/a&gt; userland command man page&lt;/li&gt;
&lt;li&gt;The difference between /etc/mtab and /proc/mounts is described well in &lt;a href="http://karelzak.blogspot.com/2011/04/bind-mounts-mtab-and-read-only.html"&gt;Karel Zak’s blog post about bind mounts &lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="http://en.wikipedia.org/wiki/Util-linux"&gt;util-linux overview&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/7997313029981170997-3633286005608369093?l=blog.endpoint.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://blog.endpoint.com/feeds/3633286005608369093/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=7997313029981170997&amp;postID=3633286005608369093' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/7997313029981170997/posts/default/3633286005608369093'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/7997313029981170997/posts/default/3633286005608369093'/><link rel='alternate' type='text/html' href='http://blog.endpoint.com/2012/01/linux-unshare-m-for-per-process-private.html' title='Linux unshare -m for per-process private filesystem mount points'/><author><name>Jon Jensen</name><uri>http://www.blogger.com/profile/18273388885281263476</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='25' src='http://bp3.blogger.com/_rFXHDrokbpE/SJHpPosaIQI/AAAAAAAAAAM/GnqeZuLItOA/S220/jon1.png'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-7997313029981170997.post-812195199102022098</id><published>2012-01-23T18:02:00.002-05:00</published><updated>2012-01-23T20:17:31.092-05:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='open-source'/><category scheme='http://www.blogger.com/atom/ns#' term='sysadmin'/><category scheme='http://www.blogger.com/atom/ns#' term='perl'/><category scheme='http://www.blogger.com/atom/ns#' term='hosting'/><category scheme='http://www.blogger.com/atom/ns#' term='networking'/><title type='text'>Our SoftLayer API tools</title><content type='html'>&lt;p&gt;We do a lot of our hosting at &lt;a href="http://www.softlayer.com/"&gt;SoftLayer&lt;/a&gt;, which seems to be one of the &lt;a href="http://www.datacenterknowledge.com/archives/2009/05/14/whos-got-the-most-web-servers/"&gt;hosts with the most servers&lt;/a&gt; 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:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a strong track record of reliability&lt;/li&gt;
&lt;li&gt;responsive support&lt;/li&gt;
&lt;li&gt;datacenters around the U.S. and some in Europe and Asia&lt;/li&gt;
&lt;li&gt;solid power backup&lt;/li&gt;
&lt;li&gt;well-connected redundant networks with multiple 10 Gbps uplinks&lt;/li&gt;
&lt;li&gt;gigabit Ethernet pipes all the way to the Internet&lt;/li&gt;
&lt;li&gt;first-class IPv6 support&lt;/li&gt;
&lt;li&gt;an internal private network with no data transfer charge&lt;/li&gt;
&lt;li&gt;Red Hat Enterprise Linux offered at no extra charge&lt;/li&gt;
&lt;li&gt;diverse dedicated server offerings at many price &amp;amp; performance points&lt;/li&gt;
&lt;li&gt;some disk partitioning options (though more flexibility here would be nice, especially with LVM for the /boot and / filesystems)&lt;/li&gt;
&lt;li&gt;fully automated provisioning, without salesman &amp;amp; quote hassles for standard offerings&lt;/li&gt;
&lt;li&gt;3000 GB data transfer per month included standard with most servers&lt;/li&gt;
&lt;li&gt;month-to-month contracts&lt;/li&gt;
&lt;li&gt;reasonable prices (though we can of course always use lower prices, we'll take quality over cheapness for most of our hosting needs!)&lt;/li&gt;
&lt;li&gt;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.)&lt;/li&gt;
&lt;li&gt;a web service API for monitoring and controlling many aspects of our account via REST/JSON or SOAP&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;(No, they're not paying me for writing this! But they really have nice offerings.)&lt;/p&gt;

&lt;p&gt;It is this last item, the SoftLayer API, that I want to elaborate on here.&lt;/p&gt;

&lt;p&gt;The &lt;a href="http://sldn.softlayer.com/"&gt;SoftLayer Development Network&lt;/a&gt; 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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;See the GitHub repository of &lt;a href="https://github.com/jonjensen/endpoint-softlayer-api"&gt;endpoint-softlayer-api&lt;/a&gt; if they would be useful to you, or to use as a starting point to interface with other SoftLayer APIs.&lt;/p&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/7997313029981170997-812195199102022098?l=blog.endpoint.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://blog.endpoint.com/feeds/812195199102022098/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=7997313029981170997&amp;postID=812195199102022098' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/7997313029981170997/posts/default/812195199102022098'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/7997313029981170997/posts/default/812195199102022098'/><link rel='alternate' type='text/html' href='http://blog.endpoint.com/2012/01/our-softlayer-api-tools.html' title='Our SoftLayer API tools'/><author><name>Jon Jensen</name><uri>http://www.blogger.com/profile/18273388885281263476</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='25' src='http://bp3.blogger.com/_rFXHDrokbpE/SJHpPosaIQI/AAAAAAAAAAM/GnqeZuLItOA/S220/jon1.png'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-7997313029981170997.post-7740547263330648939</id><published>2012-01-21T11:36:00.000-05:00</published><updated>2012-01-21T11:36:00.082-05:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='mysql'/><category scheme='http://www.blogger.com/atom/ns#' term='monitoring'/><category scheme='http://www.blogger.com/atom/ns#' term='hosting'/><category scheme='http://www.blogger.com/atom/ns#' term='Ubuntu'/><title type='text'>MySQL replication monitoring on Ubuntu 10.04 with Nagios and NRPE</title><content type='html'>&lt;p&gt;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.&lt;/p&gt;&lt;h3&gt;Getting the Nagios NRPE client setup on Ubuntu 10.04&lt;/h3&gt;&lt;p&gt;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:&lt;br /&gt;
&lt;pre&gt;sudo apt-get install nagios-nrpe-server nagios-plugins&lt;/pre&gt;&lt;/p&gt;&lt;p&gt;The NRPE configuration is stored in &lt;code&gt;/etc/nagios/nrpe.cfg&lt;/code&gt;, while the plugins are installed in &lt;code&gt;/usr/lib/nagios/plugins/&lt;/code&gt; (or lib64).  The installation of this package will also create a user &lt;code&gt;nagios&lt;/code&gt; which does not have login permissions.  After the packages are installed the first step is to make sure that &lt;code&gt;/etc/nagios/nrpe.cfg&lt;/code&gt; has some basic configuration.&lt;/p&gt;&lt;p&gt;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 &lt;code&gt;server_address&lt;/code&gt; 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.&lt;/p&gt;&lt;h3&gt;Choosing what NRPE commands you want to support&lt;/h3&gt;&lt;p&gt;Further down in the configuration, you'll see lines like &lt;code&gt;command[check_users]=/usr/lib/nagios/plugins/check_users -w 5 -c 10&lt;/code&gt;.  These are the commands you plan to offer the Nagios server to monitor.  Review the contents of &lt;code&gt;/usr/lib/nagios/plugins/&lt;/code&gt; 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!&lt;/p&gt;&lt;p&gt;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:&lt;br /&gt;
&lt;pre&gt;service nagios-nrpe-server restart&lt;/pre&gt;&lt;/p&gt;&lt;p&gt;This would also be an appropriate time to confirm that the nagios-nrpe-server service is configured to start on boot.  I prefer the &lt;code&gt;chkconfig&lt;/code&gt; package to help with this task, so if you don't already have it installed:&lt;br /&gt;
&lt;pre&gt;sudo apt-get install chkconfig
chkconfig | grep nrpe

# You should see...
nagios-nrpe-server     on

# If you don't...
chkconfig nagios-nrpe-server on
&lt;/pre&gt;&lt;/p&gt;&lt;h3&gt;Pre flight check - running check_nrpe&lt;/h3&gt;&lt;p&gt;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:&lt;br /&gt;
&lt;pre&gt;check_nrpe -H host_of_new_nrpe_client -c command_name&lt;/pre&gt;&lt;/p&gt;&lt;p&gt;If your command output something useful and expected, your on the right track.  A common error you might see: &lt;code&gt;Connection refused by host&lt;/code&gt;.  Here's a quick checklist:&lt;br /&gt;
&lt;ul&gt;&lt;li&gt;Did you start the nagios-nrpe-server service?&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;netstat -aunt&lt;/code&gt; on the NRPE client to make sure the service is listening on the right address and ports.&lt;/li&gt;
&lt;li&gt;Did you open the appropriate ports on all your firewall(s)?&lt;/li&gt;
&lt;li&gt;Is there NAT translation which needs configuration?&lt;/li&gt;
&lt;/ul&gt;&lt;/p&gt;&lt;h3&gt;Adding the check_mysql_replication plugin&lt;/h3&gt;&lt;p&gt;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 &lt;a href="https://github.com/bbuchalter/check_mysql_replication/blob/master/check_mysql_replication.sh"&gt;check_mysql_replication&lt;/a&gt;.  Pull that source down into your plugins directory (&lt;code&gt;/usr/lib/nagios/plugins/&lt;/code&gt; (or lib64)) and make sure the permissions match the other plugins.&lt;/p&gt;&lt;p&gt;With the plugin now in place, add a command to your &lt;code&gt;nrpe.cfg&lt;/code&gt;.&lt;br /&gt;
&lt;pre&gt;command[check_mysql_replication]=sudo /usr/lib/nagios/plugins/check_mysql_replication.sh -H &lt;slave_host_address&gt;&lt;/pre&gt;&lt;/p&gt;&lt;p&gt;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.&lt;br /&gt;
&lt;pre&gt;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
&lt;/pre&gt;&lt;/p&gt;&lt;p&gt;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.&lt;br /&gt;
&lt;pre&gt;nagios ALL= NOPASSWD: /usr/lib/nagios/plugins/check_mysql_replication.sh
&lt;/pre&gt;&lt;/p&gt;&lt;h3&gt;Wrapping Up&lt;/h3&gt;&lt;p&gt;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.&lt;/p&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/7997313029981170997-7740547263330648939?l=blog.endpoint.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://blog.endpoint.com/feeds/7740547263330648939/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=7997313029981170997&amp;postID=7740547263330648939' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/7997313029981170997/posts/default/7740547263330648939'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/7997313029981170997/posts/default/7740547263330648939'/><link rel='alternate' type='text/html' href='http://blog.endpoint.com/2012/01/mysql-replication-monitoring-on-ubuntu.html' title='MySQL replication monitoring on Ubuntu 10.04 with Nagios and NRPE'/><author><name>Brian Buchalter</name><uri>http://www.blogger.com/profile/01591156911422086869</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='http://2.bp.blogspot.com/-O1WazcpsfX8/TwSI332IhgI/AAAAAAAAABo/UZzjB39UXJ4/s220/headshot_close.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-7997313029981170997.post-5086300527038553385</id><published>2012-01-19T14:11:00.004-05:00</published><updated>2012-02-01T16:53:42.098-05:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='ruby'/><category scheme='http://www.blogger.com/atom/ns#' term='ecommerce'/><category scheme='http://www.blogger.com/atom/ns#' term='ruby-and-ruby-on-rails'/><title type='text'>Importing Data with RailsAdmin</title><content type='html'>&lt;p&gt;&lt;b&gt;Update: Read an update to this functionality &lt;a href="http://blog.endpoint.com/2012/02/railsadmin-import-part-2.html"&gt;here&lt;/a&gt;.&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;I've blogged about &lt;a href="https://github.com/sferik/rails_admin"&gt;RailsAdmin&lt;/a&gt; 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 &lt;a href="http://www.piggybak.org/"&gt; Ruby on Rails Ecommerce Engine (Piggybak)&lt;/a&gt;. One thing that I found lacking in RailsAdmin is the ability to import data. However, it has come up in the &lt;a href="http://groups.google.com/group/rails_admin"&gt;RailsAdmin Google Group&lt;/a&gt; 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.&lt;/p&gt;

&lt;p&gt;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:&lt;/p&gt;

&lt;h3&gt;#1: Create Controller&lt;/h3&gt;

&lt;pre&gt;
class CustomAdminController &lt; RailsAdmin::MainController
  def import
    # TODO
  end
end
&lt;/pre&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h3&gt;#2: Add import route&lt;/h3&gt;

&lt;pre&gt;
match "/admin/:model_name/import" =&gt; "custom_admin#import" , :as =&gt; "import", :via =&gt; [:get, :post]
mount RailsAdmin::Engine =&gt; '/admin', :as =&gt; 'rails_admin'
&lt;/pre&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h3&gt;#3: Override Rails Admin View&lt;/h3&gt;

&lt;p&gt;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:&lt;/p&gt;

&lt;pre&gt;
...
- can_import = authorized? :import, abstract_model

...
%li{:class =&gt; (params[:action] == 'import' &amp;&amp; 'active')}= link_to "Import", main_app.import_path(model_name) if can_import
&lt;/pre&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h3&gt;#4: CanCan Settings&lt;/h3&gt;

&lt;p&gt;My application uses &lt;a href="https://github.com/sferik/rails_admin/wiki/CanCan"&gt;CanCan with RailsAdmin&lt;/a&gt;, 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.&lt;/p&gt;

&lt;pre&gt;
if user &amp;&amp; user.is_admin?
  cannot :import, :all
  can :import, [Book, SomeModel1, SomeModel2, SomeModel3]
end
&lt;/pre&gt;

&lt;p&gt;I now see an Import tab in the admin:&lt;/p&gt;

&lt;p&gt;&lt;img style="border:1px solid #999;" src="http://3.bp.blogspot.com/-KEVmgDkTsYQ/TxYBlqkQy7I/AAAAAAAAE2k/jWlbrzkN6xI/s1600/import.png" /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;#5: Create View&lt;/h3&gt;

&lt;p&gt;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:&lt;/p&gt;

&lt;pre&gt;
&amp;lt;h1&amp;gt;Import&amp;lt;/h1&amp;gt;
&amp;lt;h2&amp;gt;Fields&amp;lt;/h2&amp;gt;
&amp;lt;ul&amp;gt;
    &amp;lt;% @abstract_model::IMPORT_FIELDS.each do |attr| -%&amp;gt;
    &amp;lt;li&amp;gt;&amp;lt;%= attr %&amp;gt;&amp;lt;/li&amp;gt;
    &amp;lt;% end -%&amp;gt;
&amp;lt;/ul&amp;gt;

&amp;lt;%= form_tag "/admin/#{@abstract_model.to_param}/import", :multipart =&gt; true do |f| -%&amp;gt;
    &amp;lt;%= file_field_tag :file %&amp;gt;
    &amp;lt;%= submit_tag "Upload", :disable_with =&gt; "Uploading..." %&amp;gt;
&amp;lt;% end -%&amp;gt;
&lt;/pre&gt;

&lt;p&gt;This will look something like this:&lt;/p&gt;

&lt;p&gt;&lt;img style="border:1px solid #999;" src="http://2.bp.blogspot.com/-eQMBV2BDYmA/TxYBlwtW_JI/AAAAAAAAE2w/0HImUyL_rO4/simport_form.png" /&gt;&lt;/p&gt;

&lt;h3&gt;#6: Import Functionality&lt;/h3&gt;

&lt;p&gt;Finally, the code for the import looks something like this:&lt;/p&gt;

&lt;pre&gt;
def import
  if request.post?
    response = { :errors =&gt; [], :success =&gt; [] }
    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] &lt;&lt; "Created: #{object.title}"
      else
        response[:error] &lt;&lt; "Failed to create: #{object.title}. Errors: #{object.errors.full_messages.join(', ')}."
      end
    end
  end
end
&lt;/pre&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h3&gt;#7: View updates to show errors&lt;/h3&gt;

&lt;p&gt;Finally, I update my view to show both success and error messages, which looks sorta like this in the view:&lt;/p&gt;

&lt;p&gt;&lt;img style="border:1px solid #999;" src="http://1.bp.blogspot.com/-DDDz3Yba4FQ/TxYBmCb4KwI/AAAAAAAAE28/U-F-lPjyTUI/import_success.png" /&gt;&lt;/p&gt;

&lt;h3&gt;Conclusion and Discussion&lt;/h3&gt;

&lt;p&gt;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 :)&lt;/p&gt;

&lt;p&gt;In reality, my application has several additional complexities, which make it less suitable for generic application:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Several of the models include attached files via &lt;a href="https://github.com/thoughtbot/paperclip"&gt;paperclip&lt;/a&gt;. Using open-uri, these files are retrieved and added to the objects.&lt;/li&gt;
&lt;li&gt;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.&lt;/li&gt;
&lt;li&gt;Several of the models require creation of a special nested object. This was model specific.&lt;/li&gt;
&lt;li&gt;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.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Read more about End Point's &lt;a href="http://www.endpoint.com/services/ruby_on_rails"&gt;Ruby on Rails development and consulting services&lt;/a&gt; or &lt;a href="http://www.endpoint.com/contact"&gt;contact us&lt;/a&gt; to help you out with a Rails project today!&lt;/p&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/7997313029981170997-5086300527038553385?l=blog.endpoint.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://blog.endpoint.com/feeds/5086300527038553385/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=7997313029981170997&amp;postID=5086300527038553385' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/7997313029981170997/posts/default/5086300527038553385'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/7997313029981170997/posts/default/5086300527038553385'/><link rel='alternate' type='text/html' href='http://blog.endpoint.com/2012/01/import-railsadmin.html' title='Importing Data with RailsAdmin'/><author><name>Steph Skardal</name><uri>http://www.blogger.com/profile/11721782860178431713</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media='http://search.yahoo.com/mrss/' url='http://3.bp.blogspot.com/-KEVmgDkTsYQ/TxYBlqkQy7I/AAAAAAAAE2k/jWlbrzkN6xI/s72-c/import.png' height='72' width='72'/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-7997313029981170997.post-514200222539115109</id><published>2012-01-14T19:39:00.000-05:00</published><updated>2012-01-17T01:49:03.135-05:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='javascript'/><category scheme='http://www.blogger.com/atom/ns#' term='ruby-and-ruby-on-rails'/><title type='text'>Using Disqus and Ruby on Rails</title><content type='html'>&lt;p&gt;Recently, I posted about how to &lt;a href="http://blog.endpoint.com/2011/12/importing-comments-into-disqus-using.html"&gt;import comments from a Ruby on Rails app to Disqus&lt;/a&gt;.  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 &lt;a href="http://docs.disqus.com/developers/universal/"&gt;Universal Code&lt;/a&gt; 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.&lt;/p&gt;&lt;h3&gt;Disqus in a development environment&lt;/h3&gt;&lt;p&gt;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 &lt;a href="http://blog.endpoint.com/2011/12/working-with-constants-in-ruby.html"&gt;Working with Constants in Ruby&lt;/a&gt;.  It might look something like this:&lt;br /&gt;
&lt;pre&gt;# app/models/article.rb

DISQUS_SHORTNAME = Rails.env == "development" ? "dev_shortname".freeze : "production_shortname".freeze

&lt;/pre&gt;&lt;/p&gt;&lt;h3&gt;Disqus Identifiers&lt;/h3&gt;&lt;p&gt;Each time you load the universal code, you need to specify a few configuration variables so that the correct thread is loaded:&lt;br /&gt;
&lt;ul&gt;&lt;li&gt;&lt;b&gt;disqus_shortname&lt;/b&gt;: tells Disqus which website account (called a forum on Disqus) this system belongs to.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;disqus_identifier&lt;/b&gt;: tells Disqus how to uniquely identify the current page.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;disqus_url&lt;/b&gt;: tells Disqus the location of the page for permalinking purposes.&lt;/li&gt;
&lt;/ul&gt;Let's create a Rails partial to set up these variables for us, so we can easily call up the appropriate comment thread.&lt;br /&gt;
&lt;pre&gt;# 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/

&amp;lt;div id=&amp;quot;disqus_thread&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;script type=&amp;quot;text/javascript&amp;quot;&amp;gt;

    var disqus_shortname = &amp;#039;&lt;%= Article::DISQUS_SHORTNAME %&gt;&amp;#039;;
    var disqus_identifier = &amp;#039;&lt;%= article.id %&gt;&amp;#039;;
    var disqus_url = &amp;#039;&lt;%= url_for(article, :only_path =&amp;gt; false) %&gt;&amp;#039;;

    /* * * DON&amp;#039;T EDIT BELOW THIS LINE * * */
    (function() {
        var dsq = document.createElement(&amp;#039;script&amp;#039;); dsq.type = &amp;#039;text/javascript&amp;#039;; dsq.async = true;
        dsq.src = &amp;#039;http://&amp;#039; + disqus_shortname + &amp;#039;.disqus.com/embed.js&amp;#039;;
        (document.getElementsByTagName(&amp;#039;head&amp;#039;)[0] || document.getElementsByTagName(&amp;#039;body&amp;#039;)[0]).appendChild(dsq);
    })();
&amp;lt;/script&amp;gt;
&lt;/pre&gt;&lt;/p&gt;&lt;p&gt;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.&lt;/p&gt;&lt;h3&gt;Disqus Identifier Gotcha&lt;/h3&gt;&lt;p&gt;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.&lt;/p&gt;&lt;h3&gt;Comment Counters&lt;/h3&gt;&lt;p&gt;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:&lt;br /&gt;
&lt;pre&gt;# HTML
&amp;lt;a href=&amp;quot;http://example.com/article1.html#disqus_thread&amp;quot; 
   data-disqus-identifier=&amp;quot;&lt;%=@article.id%&gt;&amp;quot;&amp;gt;
This will be replaced by the comment count
&amp;lt;/a&amp;gt;

# Rails helper
&lt;%= link_to "This will be replaced by the comment count", 
    article_path(@article, :anchor =&gt; "disqus_thread"), 
    :"data-disqus-identifer" =&gt; @article.id %&gt;
&lt;/pre&gt;&lt;p&gt;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.&lt;/p&gt;&lt;pre&gt;# app/view/disqus/_comment_count_javascript.html.erb
# from http://docs.disqus.com/developers/universal/
# add once per page, just above &amp;lt;/body&amp;gt;

&amp;lt;script type=&amp;quot;text/javascript&amp;quot;&amp;gt;
   
    var disqus_shortname = &amp;#039;&lt;%= Article::DISQUS_SHORTNAME %&gt;&amp;#039;;

    /* * * DON&amp;#039;T EDIT BELOW THIS LINE * * */
    (function () {
        var s = document.createElement(&amp;#039;script&amp;#039;); s.async = true;
        s.type = &amp;#039;text/javascript&amp;#039;;
        s.src = &amp;#039;http://&amp;#039; + disqus_shortname + &amp;#039;.disqus.com/count.js&amp;#039;;
        (document.getElementsByTagName(&amp;#039;HEAD&amp;#039;)[0] || document.getElementsByTagName(&amp;#039;BODY&amp;#039;)[0]).appendChild(s);
    }());
&amp;lt;/script&amp;gt;
&lt;/pre&gt;&lt;p&gt;Disqus recommends adding it just before the closing &lt;code&gt; &amp;lt;/body&amp;gt;&lt;/code&gt; 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.&lt;/p&gt;&lt;h3&gt;Styling Comment Counts&lt;/h3&gt;&lt;p&gt;Disqus provides &lt;a href="http://docs.disqus.com/help/69/"&gt;extensive CSS documentation&lt;/a&gt; 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 &gt; Appearance, I could add HTML tags around the output of the comments.&lt;/p&gt;&lt;div class="separator" style="clear: both; text-align: center;"&gt;&lt;a href="http://3.bp.blogspot.com/-1jB0r9X0CWE/TwyoUVFnc7I/AAAAAAAAACk/zhBpUkWOHBw/s1600/count.png" imageanchor="1" style="margin-left:1em; margin-right:1em"&gt;&lt;img border="0" height="131" width="400" src="http://3.bp.blogspot.com/-1jB0r9X0CWE/TwyoUVFnc7I/AAAAAAAAACk/zhBpUkWOHBw/s400/count.png" /&gt;&lt;/a&gt;&lt;/div&gt;&lt;p&gt;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.&lt;/p&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/7997313029981170997-514200222539115109?l=blog.endpoint.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://blog.endpoint.com/feeds/514200222539115109/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=7997313029981170997&amp;postID=514200222539115109' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/7997313029981170997/posts/default/514200222539115109'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/7997313029981170997/posts/default/514200222539115109'/><link rel='alternate' type='text/html' href='http://blog.endpoint.com/2012/01/using-disqus-and-rails.html' title='Using Disqus and Ruby on Rails'/><author><name>Brian Buchalter</name><uri>http://www.blogger.com/profile/01591156911422086869</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='http://2.bp.blogspot.com/-O1WazcpsfX8/TwSI332IhgI/AAAAAAAAABo/UZzjB39UXJ4/s220/headshot_close.jpg'/></author><media:thumbnail xmlns:media='http://search.yahoo.com/mrss/' url='http://3.bp.blogspot.com/-1jB0r9X0CWE/TwyoUVFnc7I/AAAAAAAAACk/zhBpUkWOHBw/s72-c/count.png' height='72' width='72'/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-7997313029981170997.post-1513854852843633871</id><published>2012-01-13T16:04:00.007-05:00</published><updated>2012-01-13T16:50:21.316-05:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='perl'/><category scheme='http://www.blogger.com/atom/ns#' term='Interchange'/><category scheme='http://www.blogger.com/atom/ns#' term='database'/><title type='text'>Interchange loops using DBI Slice</title><content type='html'>&lt;p&gt;One day I was reading through the documentation on &lt;a href="http://search.cpan.org"&gt;search.cpan.org&lt;/a&gt; for the &lt;a href="http://search.cpan.org/~timb/DBI-1.616/DBI.pm"&gt;DBI&lt;/a&gt; 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.
&lt;/p&gt;
&lt;p&gt;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:
&lt;pre&gt;&lt;code&gt;my $results = $dbh-&gt;selectall_arrayref(
  q{
    SELECT
      sku,
      description,
      price,
      thumb,
      category, 
      prod_group
    FROM
      products
    WHERE
      category = ?},
  { Slice =&gt; {} }, 
  $category
);
$::Tag-&gt;tmpn("product_list", $results);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In the actual HTML page, you would do this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;table cellpadding=0 cellspacing=2 border=1&amp;gt;
&amp;lt;tr&amp;gt;
  &amp;lt;th&amp;gt;Image&amp;lt;/th&amp;gt;
  &amp;lt;th&amp;gt;Description&amp;lt;/th&amp;gt;
  &amp;lt;th&amp;gt;Product Group&amp;lt;/th&amp;gt;
  &amp;lt;th&amp;gt;Category&amp;lt;/th&amp;gt;
  &amp;lt;th&amp;gt;Price&amp;lt;/th&amp;gt;
&amp;lt;/tr&amp;gt;
[loop object.mv_results=`$Scratch-&amp;gt;{product_list}` prefix=plist]
[list]
&amp;lt;tr&amp;gt;
  &amp;lt;td&amp;gt;&amp;lt;a href=&amp;quot;/cgi-bin/vlink/[plist-param sku].html&amp;quot;&amp;gt;&amp;lt;img src=&amp;quot;[plist-param thumb]&amp;quot;&amp;gt;&amp;lt;/a&amp;gt;&amp;lt;/td&amp;gt;
  &amp;lt;td&amp;gt;[plist-param description]&amp;lt;/td&amp;gt;
  &amp;lt;td&amp;gt;[plist-param prod_group]&amp;lt;/td&amp;gt;
  &amp;lt;td&amp;gt;[plist-param category]&amp;lt;/td&amp;gt;
  &amp;lt;td&amp;gt;[plist-param price]&amp;lt;/td&amp;gt;
&amp;lt;/tr&amp;gt;
[/list]
[/loop]
&amp;lt;/table&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We normally use this when writing ActionMaps and using some template as our setting for mv_nextpage.&lt;/p&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/7997313029981170997-1513854852843633871?l=blog.endpoint.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://blog.endpoint.com/feeds/1513854852843633871/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=7997313029981170997&amp;postID=1513854852843633871' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/7997313029981170997/posts/default/1513854852843633871'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/7997313029981170997/posts/default/1513854852843633871'/><link rel='alternate' type='text/html' href='http://blog.endpoint.com/2012/01/interchange-loops-using-dbi-slice.html' title='Interchange loops using DBI Slice'/><author><name>Richard Templet</name><uri>http://www.blogger.com/profile/00748241757007069230</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-7997313029981170997.post-6884930248444762838</id><published>2012-01-13T11:44:00.002-05:00</published><updated>2012-01-13T16:09:29.390-05:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='open-source'/><category scheme='http://www.blogger.com/atom/ns#' term='ecommerce'/><category scheme='http://www.blogger.com/atom/ns#' term='ruby-and-ruby-on-rails'/><title type='text'>ActiveRecord Callbacks for Order Processing in Ecommerce Applications</title><content type='html'>&lt;p&gt;As I recently blogged about, I introduced a new &lt;a href="http://piggybak.org"&gt;Ruby on Rails Ecommerce Engine&lt;/a&gt;. The gem relies on &lt;a href="https://github.com/sferik/rails_admin"&gt;RailsAdmin&lt;/a&gt;, 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.&lt;/p&gt;

&lt;h3&gt;Order Data Model&lt;/h3&gt;

&lt;p&gt;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:&lt;/p&gt;

&lt;pre&gt;
class Order &lt; ActiveRecord::Base
  has_many :line_items, :inverse_of =&gt; :order
  has_many :payments, :inverse_of =&gt; :order
  has_many :shipments, :inverse_of =&gt; :order
  has_many :credits, :inverse_of =&gt; :order

  belongs_to :billing_address, :class_name =&gt; "Piggybak::Address"
  belongs_to :shipping_address, :class_name =&gt; "Piggybak::Address"
  belongs_to :user
  
  accepts_nested_attributes_for :billing_address, :allow_destroy =&gt; true
  accepts_nested_attributes_for :shipping_address, :allow_destroy =&gt; true
  accepts_nested_attributes_for :shipments, :allow_destroy =&gt; true
  accepts_nested_attributes_for :line_items, :allow_destroy =&gt; true
  accepts_nested_attributes_for :payments
end
&lt;/pre&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h3&gt;Step #1: user enters data, and clicks submit&lt;/h3&gt;

&lt;div class="separator" style="clear: both; text-align: center;"&gt;
&lt;a href="http://4.bp.blogspot.com/-gF-IPr1bocY/Tw3Fp0lZ1-I/AAAAAAAAE2U/fYCLefNq1wo/s1600/800px-Steph_workspace.jpg" imageanchor="1" style="margin-left:1em; margin-right:1em"&gt;&lt;img border="0" width="750" src="http://4.bp.blogspot.com/-gF-IPr1bocY/Tw3Fp0lZ1-I/AAAAAAAAE2U/fYCLefNq1wo/800px-Steph_workspace.jpg" /&gt;&lt;/a&gt;&lt;/div&gt;


&lt;h3&gt;Step #2: before_validation&lt;/h3&gt;

&lt;p&gt;Using a before_validation ActiveRecord callback, a few things happen on the order:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Some order defaults are set&lt;/li&gt;
&lt;li&gt;The order total is reset&lt;/li&gt;
&lt;li&gt;The order total due is reset&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;Step #3: validation&lt;/h3&gt;

&lt;p&gt;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).&lt;/p&gt;

&lt;p&gt;Payments have a special validation step here. A custom validation method on the payment attributes is performed to confirm validity of the credit card:&lt;/p&gt;

&lt;pre&gt;
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? &amp;&amp; !["first_name", "last_name", "type"].include?(key)
          record.errors.add key, value
        end
      end
    end
  end
end
&lt;/pre&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h3&gt;Step #4: after_validation&lt;/h3&gt;

&lt;p&gt;Next, the after_validation callback is used to update totals. It does a few things here:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Calculates shipping costs for new shipments only.&lt;/li&gt;
&lt;li&gt;Calculates tax charge on the order.&lt;/li&gt;
&lt;li&gt;Subtracts credits on the order, if they exist.&lt;/li&gt;
&lt;li&gt;Calculates total_due, to be used by payment&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h3&gt;Step #5: before_save part 1&lt;/h3&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Examples of failures here include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Credit card transaction denied for a number of reasons&lt;/li&gt;
&lt;li&gt;Payment gateway down&lt;/li&gt;
&lt;li&gt;Payment gateway API information incorrect&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;Step #6: before_save part 2&lt;/h3&gt;

&lt;p&gt;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 :)&lt;/p&gt;

&lt;h3&gt;Step #7: Save&lt;/h3&gt;

&lt;p&gt;Finally, if everything's gone through, the order is saved.&lt;/p&gt;

&lt;h3&gt;Summary&lt;/h3&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Check out the full list of ActiveRecord callbacks &lt;a href="http://guides.rubyonrails.org/active_record_validations_callbacks.html#available-callbacks"&gt;here&lt;/a&gt;. And check out the Order model for Piggybak &lt;a href="https://github.com/stephskardal/piggybak/blob/master/app/models/piggybak/order.rb"&gt;here&lt;/a&gt;.&lt;/p&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/7997313029981170997-6884930248444762838?l=blog.endpoint.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://blog.endpoint.com/feeds/6884930248444762838/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=7997313029981170997&amp;postID=6884930248444762838' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/7997313029981170997/posts/default/6884930248444762838'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/7997313029981170997/posts/default/6884930248444762838'/><link rel='alternate' type='text/html' href='http://blog.endpoint.com/2012/01/activerecord-callbacks-ecommerce-order.html' title='ActiveRecord Callbacks for Order Processing in Ecommerce Applications'/><author><name>Steph Skardal</name><uri>http://www.blogger.com/profile/11721782860178431713</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media='http://search.yahoo.com/mrss/' url='http://4.bp.blogspot.com/-gF-IPr1bocY/Tw3Fp0lZ1-I/AAAAAAAAE2U/fYCLefNq1wo/s72-c/800px-Steph_workspace.jpg' height='72' width='72'/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-7997313029981170997.post-1516252933491266217</id><published>2012-01-10T11:40:00.001-05:00</published><updated>2012-01-10T12:33:12.967-05:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='cucumber'/><category scheme='http://www.blogger.com/atom/ns#' term='dropbox'/><category scheme='http://www.blogger.com/atom/ns#' term='ruby-and-ruby-on-rails'/><title type='text'>Take a snapshot in Cucumber and sync it with Dropbox!</title><content type='html'>&lt;p&gt;In a previous &lt;a href="http://blog.endpoint.com/2011/12/running-integration-tests-in-webkit.html"&gt;post&lt;/a&gt; I talked about running cucumber using &lt;a href="https://github.com/thoughtbot/capybara-webkit"&gt;capybara-webkit&lt;/a&gt;. In a recent project using this setup I noticed that I couldn't use capybara in connection with &lt;a href="https://github.com/copiousfreetime/launchy"&gt;launchy&lt;/a&gt; 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.&lt;/p&gt;&lt;p&gt;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 &lt;a href="http://www.dropbox.com"&gt;dropbox&lt;/a&gt; so that it gets synced to my desktop automatically when it is created.&lt;/p&gt;&lt;p&gt;Wait, seriously? Dropbox? &lt;/p&gt;&lt;p&gt;Yes, absolutely. Dropbox.&lt;/p&gt;&lt;p&gt;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.&lt;/p&gt;&lt;h3&gt;Step 1: Create a "take a snapshot" step.&lt;/h3&gt;&lt;p&gt;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 &lt;code&gt;web_steps.rb&lt;/code&gt; but you can add it to any of your &lt;code&gt;step_definitions&lt;/code&gt; files.  The step looks like this:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;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
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The first line there is fairly simple. Capybara provides a &lt;code&gt;save_page&lt;/code&gt; method by default which is going to save the page to &lt;code&gt;tmp/capybara&lt;/code&gt; off the root of your app.  The file will look something like this: &lt;code&gt;capybara-20111228210921591550991.html&lt;/code&gt;. You can see the source code &lt;a href="http://rubydoc.info/github/jnicklas/capybara/master/Capybara/Session:save_page"&gt;on the rdoc page&lt;/a&gt; for more information on how this works.  If you want to customize the file name, you can do that by calling &lt;code&gt;Capybara.save_page&lt;/code&gt; directly like this:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;Capybara.save_page(body, 'my_custom_file.html')
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This reveals what &lt;code&gt;save_page&lt;/code&gt; is actually doing. Every cucumber step has available to it by default &lt;code&gt;Capybara::Session&lt;/code&gt;. The source html of the current page is stored in &lt;code&gt;Capybara::Session#body&lt;/code&gt;. The &lt;code&gt;save_page&lt;/code&gt; method is just writing the source html to a file.&lt;/p&gt;&lt;p&gt;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 &lt;a href="https://github.com/thoughtbot/capybara-webkit/blob/master/lib/capybara/driver/webkit.rb#L106"&gt;explore the code on github&lt;/a&gt; to get more information, but &lt;a href="https://github.com/thoughtbot/capybara-webkit/blob/master/lib/capybara/driver/webkit/browser.rb#L99"&gt;basically it's calling&lt;/a&gt; the "Render" command on webkit and then storing the image as a PNG.)&lt;/p&gt;&lt;p&gt;All I'm doing here is changing out the &lt;code&gt;html&lt;/code&gt; file extension for &lt;code&gt;png&lt;/code&gt; so that the files will be easy to find. You can also pass width and height options if you'd like with a hash &lt;code&gt;{:width =&amp;gt; 1000, :height =&amp;gt; 10}&lt;/code&gt;.&lt;/p&gt;&lt;h3&gt;Step 2: Setup Dropbox.&lt;/h3&gt;&lt;p&gt;I didn't know this until recently but you can &lt;a href="https://www.dropbox.com/install?os=lnx"&gt;run dropbox on a linux server&lt;/a&gt; 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 &lt;a href="https://www.dropbox.com/install?os=lnx"&gt;web site&lt;/a&gt;:&lt;/p&gt;&lt;p&gt;For 32 bit: &lt;/p&gt;&lt;pre&gt;&lt;code&gt;cd ~ &amp;amp;&amp;amp; wget -O - http://www.dropbox.com/download?plat=lnx.x86 | tar xzf -
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;or 64 bit: &lt;/p&gt;&lt;pre&gt;&lt;code&gt;cd ~ &amp;amp;&amp;amp; wget -O - http://www.dropbox.com/download?plat=lnx.x86_64 | tar xzf -
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;You'll also need a small python script for working with the daemon.  You can download it from the web site or from &lt;a href="https://www.dropbox.com/download?dl=packages/dropbox.py"&gt;here&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;The dropbox cli will walk you through a simple authentication process and then start downloading your dropbox folder in your &lt;code&gt;~/Dropbox&lt;/code&gt; directory when you run &lt;code&gt;dropbox.py start&lt;/code&gt; 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 &lt;code&gt;~/Dropbox/cuke_snapshots&lt;/code&gt; 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):&lt;/p&gt;&lt;pre&gt;&lt;code&gt;cd ~/Dropbox
dropbox exlcude add Public Photos
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;That adds the &lt;code&gt;Public&lt;/code&gt; and &lt;code&gt;Photos&lt;/code&gt; 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.&lt;/p&gt;&lt;pre&gt;&lt;code&gt;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
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The last step is to create a symbolic link from your app into your dropbox folder. I did this with the following command:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;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
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Of course there are many ways you could set this up, but this will get the job done.&lt;/p&gt;&lt;h3&gt;Using "take a snapshot"&lt;/h3&gt;&lt;p&gt;Once you have everything setup, all you need to do is call the cucumber step from within your scenarios. Here's a contrived example:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;@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
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;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.&lt;/p&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/7997313029981170997-1516252933491266217?l=blog.endpoint.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://blog.endpoint.com/feeds/1516252933491266217/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=7997313029981170997&amp;postID=1516252933491266217' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/7997313029981170997/posts/default/1516252933491266217'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/7997313029981170997/posts/default/1516252933491266217'/><link rel='alternate' type='text/html' href='http://blog.endpoint.com/2012/01/take-snapshot-in-cucumber-and-sync-it.html' title='Take a snapshot in Cucumber and sync it with Dropbox!'/><author><name>Mike Farmer</name><uri>http://www.blogger.com/profile/17837382553105675943</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='http://1.bp.blogspot.com/_w-38sGrb254/SzFCMDr1YDI/AAAAAAAAACA/BxiRSpvIn1s/s1600-R/332ac5416102489eb1c54c57225af109.png'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-7997313029981170997.post-3657661192552993825</id><published>2012-01-06T15:44:00.005-05:00</published><updated>2012-01-09T10:04:19.443-05:00</updated><title type='text'>Introducing Piggybak: A Mountable Ruby on Rails Ecommerce Engine</title><content type='html'>&lt;p&gt;Here at End Point, we work with a variety of open source solutions, both full-fledged ecommerce applications and smaller modular tools. In our work with open source ecommerce applications, we spend a significant amount of time building out custom features, whether that means developing custom shipping calculations, product personalization, accounting integration or custom inventory management.&lt;/p&gt;

&lt;p&gt;There are advantages to working with a full-featured ecommerce platform. If you are starting from scratch, a full-featured platform can be a tool to quickly get product out the door and money in your pocket. But to do this, you must accept the assumptions that the application makes. And most generic, monolithic ecommerce platforms are created to satisfy many users.&lt;/p&gt;

&lt;p&gt;Working with a large monolithic ecommerce platform has disadvantages, too:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sometimes over-generic-izing a platform to satisfy the needs of many comes at the cost of code complexity, performance, or difficulty in customization.&lt;/li&gt;
&lt;li&gt;Occasionally, the marketing of an ecommerce solution overpromises and underdelivers, leaving users with unrealistic expectations on what they get out of the box.&lt;/li&gt;
&lt;li&gt;Customization on any of these platforms is not always simple, elegant, or cheap. And it may prevent easy maintenance or upgrades in the future. For example, building out customization A, B, and C on the platform &lt;i&gt;today&lt;/i&gt; may make it difficult to upgrade &lt;i&gt;later&lt;/i&gt; to use the added features X, Y, and Z.&lt;/li&gt;
&lt;li&gt;Users of a platform rely heavily on the maintainers of the project. This can mean that users may not be able to keep up with a fast-moving platform, or even that a slow-moving platform doesn't stay up to date with ecommerce trends.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Full-featured ecommerce platforms do make sense for a some clients. Hosted ecommerce solutions (&lt;a href="http://www.shopify.com/"&gt;Shopify&lt;/a&gt;, &lt;a href="http://www.volusion.com/"&gt;Volusion&lt;/a&gt;) even make sense for a lot of people too. Here at End Point, we try to be realistic about the pros and cons of building off of an ecommerce platform, but balance that with available tools to develop something for our clients efficiently and pragmatically.&lt;/p&gt;
 
&lt;h3&gt;Enter Rails&lt;/h3&gt;

&lt;p&gt;I like Ruby, and Ruby on Rails a lot. With &lt;a href="http://gembundler.com/"&gt;bundler&lt;/a&gt;, Rails has gotten particularly smart regarding managing dependencies. The Rails community also has a lot of great tools (gems) that can supplement an application. And a great MVC foundation plus include an improved way to manage assets (CSS, JavaScript, images) to help with performance and code organization. A few really great gems I've worked with are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="http://activemerchant.org/"&gt;ActiveMerchant&lt;/a&gt;: a Ruby library for handling payments&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/sferik/rails_admin"&gt;RailsAdmin&lt;/a&gt;: admin interface for managing data&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/mbleigh/acts-as-taggable-on"&gt;ActsAsTaggableOn&lt;/a&gt;: tagging functionality&lt;/li&gt;
&lt;li&gt;&lt;a href="http://prawn.majesticseacreature.com/"&gt;Prawn&lt;/a&gt;: a Ruby pdf generator&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/thoughtbot/paperclip"&gt;Paperclip&lt;/a&gt;, file attachment management functionality&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The other thing that's really cool about Ruby on Rails is &lt;a href="http://edgeapi.rubyonrails.org/classes/Rails/Engine.html"&gt;Rails Engines&lt;/a&gt;. Engines allow you to wrap Rails applications into modular elements that can be and easily shared across applications. And the parent application has control over the mount point of an Engine, meaning that a parent application can mount another engine at "/blog", "/shop", "/foo", "/bar" without affecting the Engine's behavior.&lt;/p&gt;

&lt;p&gt;I'm not trying to be a Rails fangirl here (I use WordPress for my personal website and still sometimes think Perl is the best tool for a job) :) But the ability to include and manage modular elements (gems, gems that are Engines) is great for building applications based on a few core modular elements.&lt;/p&gt;

&lt;h3&gt;My Story&lt;/h3&gt;

&lt;p&gt;Several weeks ago, I started putting together a prototype for a client for a Ruby on Rails ecommerce site. Their site has fairly custom needs with a complex data relationships (data model), complex search requirements, but relatively simple cart and checkout needs. I was certain that existing open source Ruby on Rails ecommerce platforms would require a significant amount of customization and would be a hassle to maintain. I also recently spent a good deal of time working with RailsAdmin on a non-ecommerce Rails project (&lt;a href="http://blog.endpoint.com/2011/08/railsadmin-gem-ecommerce.html"&gt;blogged here&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Rather than try to fight against an existing framework, I developed a prototype website using RailsAdmin. My prototype quickly developed into an ecommerce Ruby on Rails Engine, which offers basic shopping cart functionality, but it doesn't try to solve every problem for everyone. Below are some details about this gem as a Rails Engine, called &lt;a href="https://github.com/stephskardal/piggybak"&gt;Piggybak&lt;/a&gt;:&lt;/p&gt;

&lt;h3&gt;Non-Tech Details&lt;/h3&gt;
&lt;p&gt;The Piggybak gem includes the following features:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Basic shopping cart functionality for adding, updating, or removing items in a cart&lt;/li&gt;
&lt;li&gt;One page checkout, with AJAX for shipping and tax calculation&lt;/li&gt;
&lt;li&gt;Checkout for registered users or guests&lt;/li&gt;
&lt;li&gt;Configurable payment methods (via ActiveMerchant)&lt;/li&gt;
&lt;li&gt;Configurable shipping and tax calculators&lt;/li&gt;
&lt;li&gt;Admin interface for entering and managing orders&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here are a few screenshots:&lt;/p&gt;

&lt;table width="100%" cellpadding="0" cellspacing="0"&gt;
&lt;tr&gt;
&lt;td valign="top" style="padding-right:7px;"&gt;

&lt;a href="http://3.bp.blogspot.com/-KpHV7ZsxJSA/Twdd1l-T_eI/AAAAAAAAE04/QyMC5mBtWnw/s1600/demo_homepage.png" imageanchor="1" style="margin-left:1em; margin-right:1em"&gt;&lt;img border="0" width="350" src="http://3.bp.blogspot.com/-KpHV7ZsxJSA/Twdd1l-T_eI/AAAAAAAAE04/QyMC5mBtWnw/s400/demo_homepage.png" /&gt;&lt;/a&gt;
&lt;p&gt;Demo homepage. The primary Rails application includes all the code for defining product navigation. In this case, it displays featured products and product categories.&lt;/p&gt;&lt;br /&gt;&lt;br /&gt;

&lt;a href="http://4.bp.blogspot.com/-BVrAIIP5ht0/Twdd0x8H3TI/AAAAAAAAE0c/waiYOiBOoo4/s1600/checkout.png" imageanchor="1" style="margin-left:1em; margin-right:1em"&gt;&lt;img border="0" height="300" src="http://4.bp.blogspot.com/-BVrAIIP5ht0/Twdd0x8H3TI/AAAAAAAAE0c/waiYOiBOoo4/s400/checkout.png" /&gt;&lt;/a&gt;
&lt;p&gt;One page checkout&lt;/p&gt;&lt;br /&gt;&lt;br /&gt;

&lt;a href="http://2.bp.blogspot.com/-16iYu_eSv2I/Twdd3OlMA9I/AAAAAAAAE1Q/q8JPMtR1W8o/s1600/admin_dashboard.png" imageanchor="1" style="margin-left:1em; margin-right:1em"&gt;&lt;img border="0" width="350" src="http://2.bp.blogspot.com/-16iYu_eSv2I/Twdd3OlMA9I/AAAAAAAAE1Q/q8JPMtR1W8o/s400/admin_dashboard.png" /&gt;&lt;/a&gt;
&lt;p&gt;Admin dashboard: Note that distinction between the primary application and the Piggybak gem regarding left navigation.&lt;/p&gt;&lt;br /&gt;&lt;br /&gt;

&lt;a href="http://4.bp.blogspot.com/-JDfWDwU4a64/TwelUq1os9I/AAAAAAAAE1o/Cf2lSevbWn8/s1600/admin_product.png" imageanchor="1" style="margin-left:1em; margin-right:1em"&gt;&lt;img border="0" height="350" src="http://4.bp.blogspot.com/-JDfWDwU4a64/TwelUq1os9I/AAAAAAAAE1o/Cf2lSevbWn8/s400/admin_product.png" /&gt;&lt;/a&gt;
&lt;p&gt;Any item in the application can become sellable. This nested form displays in the admin for the sellable items.&lt;/p&gt;

&lt;/td&gt;
&lt;td valign="top"&gt;

&lt;a href="http://4.bp.blogspot.com/-f46FFjZau3g/Twdd2kcUNRI/AAAAAAAAE1A/7cHgGs5mkFo/s1600/demo_image.png" imageanchor="1" style="margin-left:1em; margin-right:1em"&gt;&lt;img border="0" width="300" src="http://4.bp.blogspot.com/-f46FFjZau3g/Twdd2kcUNRI/AAAAAAAAE1A/7cHgGs5mkFo/s400/demo_image.png" /&gt;&lt;/a&gt;
&lt;p&gt;The cart form added to the image page is driven by the Piggybak gem, but the rest of the page content is driven by the primary application. The gem doesn't make any decisions about what will be displayed on a standard product page. It only helps with generating the form to add something to the shopping cart.&lt;/p&gt;&lt;br /&gt;&lt;br /&gt;

&lt;a href="http://1.bp.blogspot.com/-IEpMO9qNibs/Twouoosa6RI/AAAAAAAAE14/fqrtkiP_LW4/s1600/order_dashboard.png" imageanchor="1" style="margin-left:1em; margin-right:1em"&gt;&lt;img border="0" width="350" src="http://1.bp.blogspot.com/-IEpMO9qNibs/Twouoosa6RI/AAAAAAAAE14/fqrtkiP_LW4/s1600/order_dashboard.png" /&gt;&lt;/a&gt;
&lt;p&gt;The admin interface for editing an order. An order has one billing and shipping address. It can have many line items, shipments, and payments. All of these nested elements are controlled directly on the order edit page.&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;

&lt;h3&gt;Tech Details&lt;/h3&gt;
&lt;p&gt;The Piggybak gem has the following dependencies:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Ruby on Rails 3.1+&lt;/li&gt;
&lt;li&gt;RailsAdmin&lt;/li&gt;
&lt;li&gt;Devise (which is a dependency of RailsAdmin)&lt;/li&gt;
&lt;li&gt;CanCan (the parent application takes responsibility for defining authorization rules&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is certainly not a short list of dependencies, but if you are developing a new application on Rails, you are likely already using a couple of these tools. And I highly recommend using RailsAdmin :)&lt;/p&gt;

&lt;p&gt;To get an idea of how the mountable solution works, these are the installation steps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Add to Gemfile and install with bundler&lt;/li&gt;
&lt;li&gt;Rake task for copying migrations and run migrations&lt;/li&gt;
&lt;li&gt;Mount engine in the parent's appplication config/routes.rb&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And then the following integration points are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;b&gt;acts_as_variant&lt;/b&gt; is added inside any model in your application to become sellable. This affectively assigns a relationship between your model(s) and the variants table. A variant belongs_to an item through a polymorphic relationship, and a model has_one variant. The variants table has information for it's sku, price, quantity on hand, and shopping cart display name.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;acts_as_orderer&lt;/b&gt; is added inside inside the user model in your application that owns orders (probably User).
&lt;li&gt;&lt;%= cart_form(@item) %&gt; is a helper method that displays an add to cart form&lt;/li&gt;
&lt;li&gt;&lt;%= cart_link %&gt; is a helper method that displays a link to the cart with the current number of items and total&lt;/li&gt;
&lt;li&gt;&lt;%= orders_link("Order History") %&gt; is a helper method which links to a users orders page&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;Summary&lt;/h3&gt;

&lt;p&gt;Here at End Point, we've had a few internal discussions about building ecommerce solutions based on more modular components to address the disadvantages we've seen in working with large monolithic ecommerce platforms. Rails provides a strong base for stitching modular components together easily.&lt;/p&gt;

&lt;table width="100%" cellpadding="0" cellspacing="0"&gt;
&lt;tr&gt;
&lt;td width="300"&gt;
&lt;img border="0" width="290" src="http://3.bp.blogspot.com/-3Z0qMBosLDY/Two2j5ATYzI/AAAAAAAAE2E/Z4bF4EvSoFI/s400/pig.jpg" /&gt;&lt;br /&gt;
&amp;copy; Steph Skardal
&lt;/td&gt;
&lt;td&gt;

&lt;p&gt;My goal with this tool was to write a modular cart and checkout component that takes advantage of RailsAdmin's DSL to provide a nice Admin interface for nested forms for models that already existing in your application. It wasn't created to solve every ecommerce problem, or world peace. As it's name indicates, this gem &lt;b&gt;piggybak&lt;/b&gt;'s off of an existing Rails application.&lt;/p&gt;

&lt;p&gt;It leaves the decision of all product finding methods and product navigation up to the developer, which means it might be great for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A deal of the day site that controls items offered daily. Deal of the day sites often don't fit the standard mold of ecommerce so ecommerce platforms don't always suit them well. And they may need significant performance customization, which is not always a feature included in a generic ecommerce platform.&lt;/li&gt;
&lt;li&gt;An ecommerce site with complex demands for search, where existing ecommerce solutions aren't easily integrated with those search solutions. A developer may build their own custom search solution and mount piggybak for handling cart and checkout only.&lt;/li&gt;
&lt;li&gt;An ecommerce site with a complex data model, where multiple types of items with varied navigation. This gem gives the functionality to turn any item on a site into a sellable item (or variant).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Demo and more documentation is forthcoming. View the repository &lt;a href="https://github.com/stephskardal/piggybak"&gt;here&lt;/a&gt;. Contributions (see TODO) appreciated. As I work through this project for the client in the next few weeks, I'm sure that there'll be a few minor bugs to work out.&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;br /&gt;&lt;br /&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/7997313029981170997-3657661192552993825?l=blog.endpoint.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://blog.endpoint.com/feeds/3657661192552993825/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=7997313029981170997&amp;postID=3657661192552993825' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/7997313029981170997/posts/default/3657661192552993825'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/7997313029981170997/posts/default/3657661192552993825'/><link rel='alternate' type='text/html' href='http://blog.endpoint.com/2012/01/piggybak-mountable-ecommerce-ruby-on.html' title='Introducing Piggybak: A Mountable Ruby on Rails Ecommerce Engine'/><author><name>Steph Skardal</name><uri>http://www.blogger.com/profile/11721782860178431713</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media='http://search.yahoo.com/mrss/' url='http://3.bp.blogspot.com/-KpHV7ZsxJSA/Twdd1l-T_eI/AAAAAAAAE04/QyMC5mBtWnw/s72-c/demo_homepage.png' height='72' width='72'/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-7997313029981170997.post-7157028004985963344</id><published>2012-01-05T13:28:00.006-05:00</published><updated>2012-01-05T14:10:55.506-05:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='open-source'/><category scheme='http://www.blogger.com/atom/ns#' term='social-networking'/><category scheme='http://www.blogger.com/atom/ns#' term='ecommerce'/><category scheme='http://www.blogger.com/atom/ns#' term='hosting'/><category scheme='http://www.blogger.com/atom/ns#' term='postgres'/><category scheme='http://www.blogger.com/atom/ns#' term='Interchange'/><category scheme='http://www.blogger.com/atom/ns#' term='database'/><title type='text'>Some great press for College District</title><content type='html'>&lt;p&gt;College District has been getting some positive press lately, the most recent being a &lt;a href="http://www.forbes.com/sites/chrissmith/2011/12/19/college-footballs-biggest-entrepreneur/" target="_blank"&gt;Forbes article&lt;/a&gt; which talks about the success they have been seeing in the last few years.&lt;/p&gt;

&lt;p&gt;College District is a company that sells collegiate merchandise to fans.  They got their start focusing on the LSU Tigers at &lt;a href="http://www.tigerdistrict.com" target="_blank"&gt;TigerDistrict.com&lt;/a&gt; and have branched out to teams such as the &lt;a href="http://www.duckdistrict.com" target="_blank"&gt;Oregon Ducks&lt;/a&gt; and &lt;a href="http://www.rolltidedistrict.com" target="_blank"&gt;Alabama Roll Tide&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;We've been working with Jared Loftus @ College District for more then four and a half years.  College District is running on a heavily modified Interchange system with some cool Postgres tricks.  The system can support a nearly unlimited number of sites, running on 2 catalogs (1 for the admin, 1 for the front end) and 1 database.  The key to the system is different schemas, fronted by views, that hide and expose records based on the database user that is connected.  The great thing about this system is that Jared can choose to launch a new store within a day and be ready for sales, something he has taken advantage of in the past when a team is on fire and he sees an opportunity he can't pass up.&lt;/p&gt;

&lt;p&gt;We are currently preparing for a re-launch of the College District site that will focus on crowd-sourced designs.  Artists and fans will submit their designs, have them voted on, some will be chosen to be sold and the folks that have their designs chosen will get paid for their efforts.  The goal here is to grow a community that guides what College District and the individual school sites ultimately sell.&lt;/p&gt;

&lt;p&gt;With College District's quick growth we've also been helping them improve their order fulfillment process.  This includes streamlining how orders are picked, packed and shipped.  The introduction of bar code scanners will help with the accuracy and speed of the process.&lt;/p&gt;

&lt;p&gt;We get a kick out of seeing our clients succeed, especially those that come to us with a clear vision and a good attitude, and then put the hard work in to make it happen.  It's an exciting year ahead for College District and we'll be right there supporting them on the journey.&lt;/p&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/7997313029981170997-7157028004985963344?l=blog.endpoint.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://blog.endpoint.com/feeds/7157028004985963344/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=7997313029981170997&amp;postID=7157028004985963344' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/7997313029981170997/posts/default/7157028004985963344'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/7997313029981170997/posts/default/7157028004985963344'/><link rel='alternate' type='text/html' href='http://blog.endpoint.com/2012/01/some-great-press-for-college-district.html' title='Some great press for College District'/><author><name>Ron Phipps</name><uri>http://www.blogger.com/profile/02106747784162431265</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='23' src='http://bp1.blogger.com/_qTrL2g2nY1M/SHFY1QqZ-YI/AAAAAAAAAAM/T6MCGHloJXU/S220/img_ron_240.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-7997313029981170997.post-4519346611570547492</id><published>2012-01-05T11:53:00.004-05:00</published><updated>2012-01-05T12:03:58.673-05:00</updated><title type='text'>Ruby on Rails: Attributes Management through Rights</title><content type='html'>&lt;p&gt;I've written a couple times about a large Rails project that a few End Pointers have been working on. The system has a fairly robust system for managing rights and accessibility. The basic data model for rights, users, groups, and roles is this:&lt;/p&gt;

&lt;img border="0" height="337" width="737" src="http://4.bp.blogspot.com/-o14m8WeuIsA/TwXPMpB6ydI/AAAAAAAAEzQ/uCdUWykSUUM/rights_data_modell.png" /&gt;

&lt;p&gt;In this data model, right_assignments belong to a single right, and belong to a subject, through a polymorphic relationship. The subject can be a Role, Group, or User. A User can have a role or belong to a group. This allows for the ability to assign a right to a user directly or through a group or role.&lt;/p&gt;

&lt;p&gt;In our code, there is a simple method for grabbing a user's set of rights:&lt;/p&gt;

&lt;pre&gt;
  class User &lt; ActiveRecord::Base
    ...
    def all_rights
      rights = [self.rights +
                self.groups.collect { |g| g.allowed_rights } +
                self.roles.collect { |r| r.rights }]
      rights = rights.flatten.uniq.collect { |r| r.action }

      rights
    end 
    ...
  end
&lt;/pre&gt;

&lt;p&gt;In the case of this data model, groups also have a boolean which specifies whether or not rights can be assigned to them. The allowed_rights method looks like this:&lt;/p&gt;

&lt;pre&gt;
  class Group &lt; ActiveRecord::Base
    ...
    def allowed_rights
      self.assignable_rights ? self.rights : []
    end 
    ...
  end
&lt;/pre&gt;

&lt;p&gt;This additional layer of protection on the group rights is because groups represent a collection of users that have implied behavior attached to them (different from roles). A group may or may not have rights assigned only through a user with the ability to assign rights to groups.&lt;/p&gt;

&lt;p&gt;The interesting part of this application is how the rights are used. In Rails, you can define the accessible attributes for an object on the fly. In our code, rights may translate to attributes that can be updated. For example, the following [extremely simplified] example demonstrates this:&lt;/p&gt;

&lt;pre&gt;
  # controller
  def update # or create
    if @item.custom_save(params)
      # saved
    else
      # handle errors
    end
  end

  # Given an item, with attributes title, description, tags, origin
  class Item &lt; ActiveRecord::Base
    ...
    def custom_save(parameters)
      self.accessible = self.attr_accessible_for(current_user)

      item.update_attributes(parameters)
    end

    def attr_accessible_for(user)
      attrs = [:title, :description]  # Defaults

      [:tags, :origin].each do |field|
        # check for if user can_set_(tags || origin)
        if current_user.all_rights.include?("can_set_#{field.to_s}")
          attrs &lt;&lt; field
        end
      end
 
      attrs
    end
    ...
  end
&lt;/pre&gt;

&lt;p&gt;The above block updates an existing item. The default accessible attributes are title and description, which means that anyone can set those values. Tags and origin can only be set if the user has the right that correspond to that attribute. Tags and origin will not be saved for a user without those rights, even if they are passed in the parameters. The obvious disadvantage with this method is that there's no exception handling when a user tries to submit parameters that they cannot set.&lt;/p&gt;

&lt;p&gt;The method above is reused to define which fields are editable to the current user, with the code shown below. So in theory, a user submitting parameters that they can't edit would only be doing it through a [malicious] post not based on the view below.&lt;/p&gt; 

&lt;pre&gt;
  # controller
  def new
    @item = Item.new

    @accessible = attr_accessible_for(current_user)
  end

  # view
  &lt;%= form_for @item do |f| %&gt;
    &lt;% [:title, :description, :tag, :origin].each do |field| %&gt;
      &lt;%= f.label field %&gt;

      &lt;% if @accessible.include?(field) %&gt;
        &lt;%= f.text_field %&gt;
      &lt;% else -%&gt; 
        &lt;%= @item.send(field) %&gt;
      &lt;% end -%&gt;

    &lt;% end -%&gt;
  &lt;% end -%&gt;
&lt;/pre&gt; 

&lt;p&gt;The data model and rights management described in this article isn't novel, but applying it in this fashion is elegant and produces modular and reusable methods. The code shown here has been simplified for this blog post. In reality, there are a few additional complexities:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The application utilizes &lt;a href="https://github.com/be9/acl9"&gt;acl9&lt;/a&gt; for access control, which is an additional layer of security that will prevent non-registered users from creating items, and will prohibit specific users from updating existing items.&lt;/li&gt;
&lt;li&gt;The user's all_rights method utilizes Rails low-level caching, with appropriate cache invalidation when the user's rights, groups, or roles change. I've given a simple example of Rails low-level caching &lt;a href="http://blog.endpoint.com/2011/09/ruby-on-rails-performance-overview.html"&gt;in this blog article&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;The logic in attr_accessible_for is more complex and can be based on the object's values or parameters. For example, an item may have a boolean that indicates anyone can tag it. The attr_accessible_for method will incorporate additional logic to determine if the :tags field is editable.&lt;/li&gt;
&lt;li&gt;The view handles different field types (checkbox, textarea, etc) and allows for overriding the field label.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here are several articles related to the same large Ruby on Rails project, for your reading pleasure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="http://blog.endpoint.com/2011/12/sunspot-solr-rails-working-with-results.html"&gt;Working with Solr Results in Rails&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="http://blog.endpoint.com/2011/11/performing-bulk-edits-in-rails-part-1.html"&gt;Performing Bulk Edits in Rails: Part 1&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="http://blog.endpoint.com/2011/11/performing-bulk-edits-in-rails-part-2.html"&gt;Performing Bulk Edits in Rails: Part 2&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="http://blog.endpoint.com/2011/11/advanced-rights-roles-management-rails.html"&gt;Advanced Rights and Role Management in Rails&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="http://blog.endpoint.com/2011/11/double-habtm-relationship-between.html"&gt;Double Has and Belongs to Many Relationship in Rails&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Or, &lt;a href="http://www.endpoint.com/services"&gt;read about our services&lt;/a&gt; and &lt;a href="http://www.endpoint.com/contact"&gt;contact us&lt;/a&gt; to help with your next Rails project.&lt;/p&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/7997313029981170997-4519346611570547492?l=blog.endpoint.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://blog.endpoint.com/feeds/4519346611570547492/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=7997313029981170997&amp;postID=4519346611570547492' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/7997313029981170997/posts/default/4519346611570547492'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/7997313029981170997/posts/default/4519346611570547492'/><link rel='alternate' type='text/html' href='http://blog.endpoint.com/2012/01/ruby-on-rails-rights-attributes.html' title='Ruby on Rails: Attributes Management through Rights'/><author><name>Steph Skardal</name><uri>http://www.blogger.com/profile/11721782860178431713</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media='http://search.yahoo.com/mrss/' url='http://4.bp.blogspot.com/-o14m8WeuIsA/TwXPMpB6ydI/AAAAAAAAEzQ/uCdUWykSUUM/s72-c/rights_data_modell.png' height='72' width='72'/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-7997313029981170997.post-6943901171324295535</id><published>2012-01-03T11:46:00.002-05:00</published><updated>2012-01-03T12:45:07.013-05:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='tools'/><category scheme='http://www.blogger.com/atom/ns#' term='hosting'/><title type='text'>Automating removal of SSH key patterns</title><content type='html'>&lt;p&gt;Every now and again, it becomes necessary to remove a user's SSH key from a system.  At End Point, we'll often allow multiple developers into multiple user accounts, so cleaning up these keys can be cumbersome.  I decided to write a shell script to brush up on those skills, make sure I completed my task comprehensively, and automate future work.&lt;/p&gt;&lt;h3&gt;Initial Design and Dependencies&lt;/h3&gt;&lt;p&gt;My plan for this script is to accept a single &lt;pattern&gt; argument which would be used to search the system's authorized_keys files.  If the pattern was found, it would offer you the opportunity to delete the line of the file on which the pattern was found.&lt;/p&gt;&lt;p&gt;I've always found &lt;code&gt;mlocate&lt;/code&gt; to be very helpful; it makes finding files extremely fast and it's usage is trivial.  For this script, we'll use the output from &lt;code&gt;locate&lt;/code&gt; to find all authorized_keys files in the system.  Of course, we'll want to make sure that the mlocate.db has recently been updated.  So let's show the user when the database was last updated and offer them a chance to update it.&lt;/p&gt;&lt;pre&gt;mlocate_path="/var/lib/mlocate/mlocate.db"
if [ -r $mlocate_path ]
then
    echo -n "mlocate database last updated: "
    stat -c %y $mlocate_path
    echo -n "Do you want to update the locate database this script depends on? [y/n]: "
    read update_locate
    if [ "$update_locate" = "y" ]
    then
        echo "Updating locate database.  This may take a few minutes..."
        updatedb
        echo "Update complete."
    fi  
else
    echo "Cannot read the mlocate db path: $mlocate_path"
    exit 2
fi
&lt;/pre&gt;&lt;p&gt;First we define the path where we can find the mlocate database.  Then we check to see if we can read that file.  If we can't read the file, we let the user know and exit.  If we can read the file, print the date and time it was last modified and offer the user a chance to update the database.  While this is functional, it's pretty brittle.  Let's make things a bit more flexible by letting &lt;code&gt;locate&lt;/code&gt; tell us where its database is.&lt;/p&gt;&lt;pre&gt;if
    mlocate_path=`locate -S`
then
    # locate -S command will output database path in following format:
    # Database /full/path/to/db: (more output)...
    mlocate_path=${mlocate_path%:*} #remove content after colon
    mlocate_path=${mlocate_path#'Database '*} #remove 'Database '
else
    echo "Couldn't run locate command.  Is mlocate installed?"
    exit 5
fi
&lt;/pre&gt;&lt;p&gt;Instead of hard coding the path to the database, we collect the locate database statics using the &lt;code&gt;-S&lt;/code&gt; parameter.  By using some &lt;a href="http://tldp.org/LDP/abs/html/string-manipulation.html"&gt;string manipulation functions&lt;/a&gt; we can tease out the file path from the output.&lt;/p&gt;&lt;p&gt;Because we are going to offering to update the location database (as well as eventually manipulating authorized_keys files), it makes sense to check that we are root before proceeding.  Additionally, let's check to see that we get a pattern form our user, and provide some usage guidance.&lt;/p&gt;&lt;pre&gt;if [ ! `whoami` = "root" ]
then
    echo "Please run as root."
    exit 4
fi

if [ -z $1 ]
then
    echo "Usage: check_authorized_keys PATTERN"
    exit 3
fi
&lt;/pre&gt;&lt;h3&gt;Checking and modifying authorization_keys for a pattern&lt;/h3&gt;&lt;p&gt;With some prerequisites in place, we're finally ready to scan the system's authorized keys files.  Let's just start with the syntax for that loop.&lt;/p&gt;&lt;pre&gt;for key_file in `locate authorized_keys`; do
    echo "Searching $key_file..."
done
&lt;/pre&gt;&lt;p&gt;We do not specify a dollar sign ($) in front of key_file when defining the loop, but once inside our loop we use the regular syntax.  We use &lt;a href="http://pubs.opengroup.org/onlinepubs/009695399/utilities/xcu_chap02.html#tag_02_06_03"&gt;command substitution&lt;/a&gt; by placing a command around back quotes (`) around the output of the command we want to use.  We're now scanning each file, but how do we find matching entries?&lt;/p&gt;&lt;pre&gt;IFS=$'\n'
for matching_entry in `grep "$1" $key_file`; do
    IFS=' '
    echo "Found an entry in $key_file:"
    echo $matching_entry
done
&lt;/pre&gt;&lt;p&gt;For each $key_file, we now grep our user's pattern ($1) and store it in $matching_entry.  We have to change the &lt;a href="http://pubs.opengroup.org/onlinepubs/009695399/utilities/xcu_chap02.html#tag_02_05_03"&gt;Input Field Seperator (IFS)&lt;/a&gt; to a new line, instead of the default space, in order to capture each grepped line in its entriety.  (Thanks to &lt;a href="http://www.endpoint.com/team/brian_miller"&gt;Brian Miller&lt;/a&gt; for that one!)&lt;/p&gt;&lt;p&gt;With a matching entry found in a key file, it's time to finally offer the user a chance to remove the entry.&lt;/p&gt;&lt;pre&gt;echo "Found an entry in $key_file:"
echo $matching_entry
echo -n "Remove entry? [y/n]: "
read remove_entry
if [ "$remove_entry" = "y" ]
then
    if [ ! -w $key_file ]
    then
        echo "Cannot write to $key_file."
     exit 1
 else
     sed -i "/$matching_entry/d" $key_file
     echo "Deleted."
 fi
else
    echo "Not deleted."
fi
&lt;/pre&gt;&lt;p&gt;We prompt the user if they want to delete the shown entry, verify we can write to the $key_file, and then delete the $matching entry.  By using the &lt;code&gt;-i&lt;/code&gt; option to the &lt;code&gt;sed&lt;/code&gt; command, we are able to make modifications inline.&lt;/p&gt;&lt;h3&gt;The Final Product&lt;/h3&gt;&lt;p&gt;I'm sure there is a lot of room for improvement on this script and I'd welcome pull requests on the &lt;a href="https://github.com/bbuchalter/clean_authorized_keys/blob/master/clean_authorized_keys"&gt;Github repo&lt;/a&gt; I setup for this little block of code.  As always, be *very* careful when running automated scripts as root.  Please test this script out on a non-production system before use.&lt;/p&gt;&lt;pre&gt;#!/bin/bash

if [ ! `whoami` = "root" ]
then
    echo "Please run as root."
    exit 4
fi


if [ -z $1 ]
then
    echo "Usage: check_authorized_keys PATTERN"
    exit 3
fi

if
    mlocate_path=`locate -S`
then
   # locate -S command will output database path in following format:
   # Database /full/path/to/db: (more output)...
   mlocate_path=${mlocate_path%:*} #remove content after colon
   mlocate_path=${mlocate_path#'Database '*} #remove 'Database '
else
    echo "Couldn't run locate command.  Is mlocate installed?"
    exit 5
fi

if [ -r $mlocate_path ]
then
    echo -n "mlocate database last updated: "
    stat -c %y $mlocate_path
    echo -n "Do you want to update the locate database this script depends on? [y/n]: "
    read update_locate
    if [ "$update_locate" = "y" ]
    then
        echo "Updating locate database.  This may take a few minutes..."
        updatedb
        echo "Update complete."
        echo ""
    fi
else
    echo "Cannot read from $mlocate_path"
    exit 2
fi

for key_file in `locate authorized_keys`; do
    echo "Searching $key_file..."
    IFS=$'\n'
    for matching_entry in `grep "$1" $key_file`; do
    IFS=' '
        echo "Found an entry in $key_file:"
        echo $matching_entry
        echo -n "Remove entry? [y/n]: "
        read remove_entry
        if [ "$remove_entry" = "y" ]
        then
            if [ ! -w $key_file ]
            then
                echo "Cannot write to $key_file."
                exit 1
            else
                sed -i "/$matching_entry/d" $key_file
                echo "Deleted."
            fi
        else
            echo "Not deleted."
        fi
    done
done

echo "Search complete."
&lt;/pre&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/7997313029981170997-6943901171324295535?l=blog.endpoint.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://blog.endpoint.com/feeds/6943901171324295535/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=7997313029981170997&amp;postID=6943901171324295535' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/7997313029981170997/posts/default/6943901171324295535'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/7997313029981170997/posts/default/6943901171324295535'/><link rel='alternate' type='text/html' href='http://blog.endpoint.com/2012/01/automating-removal-of-ssh-key-patterns.html' title='Automating removal of SSH key patterns'/><author><name>Brian Buchalter</name><uri>http://www.blogger.com/profile/01591156911422086869</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='http://2.bp.blogspot.com/-O1WazcpsfX8/TwSI332IhgI/AAAAAAAAABo/UZzjB39UXJ4/s220/headshot_close.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-7997313029981170997.post-8400749592058577300</id><published>2012-01-02T15:16:00.003-05:00</published><updated>2012-01-02T15:24:29.968-05:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='performance'/><category scheme='http://www.blogger.com/atom/ns#' term='caching'/><category scheme='http://www.blogger.com/atom/ns#' term='seo'/><category scheme='http://www.blogger.com/atom/ns#' term='search'/><category scheme='http://www.blogger.com/atom/ns#' term='scalability'/><category scheme='http://www.blogger.com/atom/ns#' term='optimization'/><category scheme='http://www.blogger.com/atom/ns#' term='Interchange'/><category scheme='http://www.blogger.com/atom/ns#' term='database'/><title type='text'>Interchange Search Caching with "Permanent More"</title><content type='html'>&lt;p&gt;Most sites that use Interchange take advantage of Interchange's "more lists". These are built-in tools that support an Interchange "search" (either the search/scan action, or result of direct SQL via [query]) to make it very easy to paginate results. Under the hood, the more list is a drill-in to a cached "search object", so each page brings back a slice from the cache of the original search. There are extensive ways to modify the look and behavior of more lists and, with a bit of effort, they can be configured to meet design requirements.&lt;/p&gt;
&lt;p&gt;Where more lists tend to fall short, however, is with respect to SEO. There are two primary SEO deficiencies that get business stakeholders' attention:&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;There is little control over the construction of the URLs for more lists. They leverage the scan actionmap and contain a hash key for the search object and numeric data to identify the slice and page location. They possess no intrinsic value in identifying the content they reference.&lt;/li&gt;
&lt;li&gt;The search cache by default is ephemeral and session-specific. This means all those results beyond page 1 the search engine has cataloged will result in dead links for search users who try to land directly on the more-listed pages.&lt;/li&gt;&lt;/ul&gt;
&lt;p&gt;It is the latter issue that I wish to address because there is--and has been for some time now--a simple mechanism called "permanent more" to remedy the default behavior.&lt;/p&gt;
&lt;p&gt;You can leverage "permanent more" by adding the boolean &lt;b&gt;mv_more_permanent&lt;/b&gt;, or the shorthand &lt;b&gt;pm&lt;/b&gt;, to your search conditions. E.g.:&lt;/p&gt;
&lt;pre&gt;
Link:

    &amp;lt;a href="[area search="
        co=1
        sf=category
        se=Foo
        op=rm
        more=1
        ml=5
        &lt;b&gt;pm=1&lt;/b&gt;
    "]"&amp;gt;All Foos&amp;lt;/a&amp;gt;

Loop:

    [loop search="
        co=1
        sf=category
        se=Foo
        op=rm
        more=1
        ml=5
        &lt;b&gt;pm=1&lt;/b&gt;
    "]
    ...loop body with [more-list]...
    [/loop]

Query:

    [query
        list=1
        more=1
        ml=10
        &lt;b&gt;pm=1&lt;/b&gt;
        sql="SELECT * FROM products WHERE category LIKE '%Foo%'"
    ]
    ...same as loop but with 10 matches/page...
    [/query]
&lt;/pre&gt;
&lt;p&gt; If the initial search is defined with the "permanent more" setting, it will produce the following adjustments:&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;The hash key used to store and identify the search cache is deterministic based on the search conditions. Many searches for Interchange are category driven. Thus, all end users who wish to browse a category end up clicking identical links, which create duplicate search caches, belonging uniquely to them. With permanent more, they all share the same cache, with the same identifier. As long as the search conditions don't change, neither does the cache identifier. Even as the cache is refreshed with new executions of the search, the object remains in the same location. Thus, the results a search engine produced this morning reference links still valid now, tomorrow, or next week, provided they reference the same search conditions.&lt;/li&gt;
&lt;li&gt;The cached search object has no session affinity. Any link referencing the cache with the correct hash key has access to the content.&lt;/li&gt;&lt;/ul&gt;
&lt;p&gt;Taken together, "permanent more" removes (for the most part, addressed later) dead links from more lists cataloged by search engines. There are, however, other benefits to "permanent more" beyond those intended as described above:&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;As stated in passing, standard Interchange search caching produces duplicate search objects for common search conditions. For a busy site, these caches can have an impact on storage. Typically, maintenance is implemented to clean up cache files for all such files whose age exceeds by some amount the session duration (standard is 48 hours). With permanent more, duplicate caches are eliminated. A cache location is reused by all users with the same search requirements, keeping data-storage requirements for caches to the minimum necessary. As searches change, ophaned caches can still easily be cleaned up as they will immediately start to age with no more access to them necessary for storage.&lt;/li&gt;
&lt;li&gt;For the same reason that "permanent more" resolves search-engine links, it also resolves content management for individual sites using a reverse proxy for caching. Because most (and certainly the easiest) caching keys are based off of URL, the deterministic nature of the hash keys for "permanent more" allows assurance that the cached content in the proxy accurately reflects the search content over time, and that all users will hit the cached resource and not generate new, unique links with varying hash keys.&lt;/li&gt;&lt;/ul&gt;
&lt;p&gt;One shortcoming of "permanent more" to be aware of is the impact of changing data underneath the search. Even if search conditions do not change, the count and order of matching record sets may. So, e.g., enough products may be removed from a given category to cause the last page of a more list to become empty, which would cause any specific link into that page to become dead. More minor, but still a possibility, is the introduction or removal of products so that a particularly searched-for term has been "bumped" to another page within the search cache since the last time the search engine crawled the more lists. For searches backed by particularly volatile data, "permanent more" may not be sufficient to address search-engine or caching demands.&lt;/p&gt;
&lt;p&gt;Finally, "permanent more" should be avoided for any search features that may cache data sensitive to an individual user. This is unlikely to happen as, under most circumstances, the configuration of the search itself will change based on the unique characteristics of the user executing the search (e.g., a username included in a query to review order history). However, it is still possible that context-sensitive information could be stored in the search object and, if so, all other users with access to the more lists would have access to that information.&lt;/p&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/7997313029981170997-8400749592058577300?l=blog.endpoint.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://blog.endpoint.com/feeds/8400749592058577300/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=7997313029981170997&amp;postID=8400749592058577300' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/7997313029981170997/posts/default/8400749592058577300'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/7997313029981170997/posts/default/8400749592058577300'/><link rel='alternate' type='text/html' href='http://blog.endpoint.com/2012/01/interchange-search-caching-with.html' title='Interchange Search Caching with &quot;Permanent More&quot;'/><author><name>Mark Johnson</name><uri>http://www.blogger.com/profile/16567331108344026567</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-7997313029981170997.post-1503871732819562242</id><published>2011-12-27T09:33:00.001-05:00</published><updated>2011-12-27T14:45:49.764-05:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='ruby'/><category scheme='http://www.blogger.com/atom/ns#' term='ruby-and-ruby-on-rails'/><title type='text'>Importing Comments into Disqus using Rails</title><content type='html'>&lt;p&gt;It seems everything is going to the cloud, even comment systems for blogs.  Disqus is a platform for offloading the ever growing feature set users expect from commenting systems.  Their website boasts over a million sites using their platform and offers a robust feature set and good performance.  But before you can drink the Kool-Aid, you've got to get your data into their system.&lt;/p&gt;&lt;p&gt;If you're using one of the common blog platforms such as WordPress or Blogger, there are fairly direct routes Disqus makes available for automatically importing your existing comment content.  For those with an unsupported platform or a hand-rolled blog, you are left with exporting your comments into XML using WordPress's WXR standard.&lt;/p&gt;&lt;p&gt;Disqus leaves a lot up to the exporter, providing only one page in there knowledge base for using what they describe as a &lt;a href="http://docs.disqus.com/developers/export/import_format/"&gt;Custom XML Import Format&lt;/a&gt;.  In my experience the import error messages were cryptic and my email support request is still unanswered 5 days later.  (Ok, so it was Christmas weekend!)&lt;/p&gt;&lt;p&gt;So let's get into the nitty gritty details.  First, the sample code provided in this article is based on Rails 3.0.x, but should work with Rails 3.1.x as well.  Rails 2.x would work just as well by modifying the way the Rails environment is booted in the first lines.  I chose to create a script to dump the output to standard output which could be piped in to a file for upload.  Let's see some of the setup work.&lt;/p&gt;&lt;h3&gt;Setting up a Rails script&lt;/h3&gt;&lt;p&gt;I choose to place the script in the &lt;code&gt;RAILS_ROOT/scripts&lt;/code&gt; directory and named it &lt;code&gt;wxr_export.rb&lt;/code&gt;.  This would allow me to call the script with the Rails 2.x style syntax (ahh, the nostalgia):&lt;/p&gt;&lt;pre&gt;script/wxr_export.rb &gt; comments.xml
&lt;/pre&gt;&lt;p&gt;This would fire up the full Rails enviornment, execute our Ruby code, and pipe the standard output to a file called comments.xml.  Pretty straightforward, but it's not that often Rails developers think about creating these kind of scripts, so it's worth discussing to see the setup mechanics.&lt;/p&gt;&lt;pre&gt;#!/usr/bin/env ruby
require File.expand_path('../../config/boot', __FILE__)
require File.expand_path('../../config/environment', __FILE__)
&lt;/pre&gt;&lt;p&gt;I think the first line is best explained by this excerpt from &lt;a href="http://en.wikibooks.org/wiki/Ruby_Programming/Hello_world#Using_env"&gt;Ruby Programming&lt;/a&gt;:&lt;/p&gt;&lt;blockquote&gt;First, we use the env command in the shebang line to search for the ruby executable in your PATH and execute it.  This way, you will not need to change the shebang line on all your Ruby scripts if you move them to a computer with Ruby installed a different directory.&lt;/blockquote&gt;&lt;p&gt;The next two lines are essentially asking the script to boot the correct Rails environment (development, testing, production).  It's worth briefly offering an explanation of the syntax of these two somewhat cryptic lines.  &lt;a href="http://ruby-doc.org/core-1.9.3/File.html#method-c-expand_path"&gt;File#expand_path&lt;/a&gt; converts a pathname to an absolute pathname.  If passed only the first string, it would use the current working path to evaluate, but since we pass &lt;code&gt;__FILE__&lt;/code&gt; we are asking it to use the current file's path as the starting point.&lt;/p&gt;&lt;p&gt;The config/boot.rb file is well documented in the &lt;a href="http://guides.rubyonrails.org/initialization.html#config-boot-rb"&gt;Rails guides&lt;/a&gt; which explains that boot.rb defines the location of your Gemfile, hooks up Bundler, which adds the dependencies of the application (including Rails) to the load path, making them available for the application to load.&lt;/p&gt;&lt;p&gt;The config/enviornment.rb file is also &lt;a href="http://guides.rubyonrails.org/initialization.html#config-environment-rb"&gt;well documented&lt;/a&gt; and effectively loads the Rails packages you've specified, such as ActiveModel, ActiveSupport, etc...&lt;/p&gt;&lt;h3&gt;Exporting WXR content&lt;/h3&gt;&lt;p&gt;Having finally loaded our Rails enviornment in a way we can use it, we are ready to actually build the XML we need.  First, let's setup our XML and the gerenal format we'll use to popualate our file:&lt;/p&gt;&lt;pre&gt;# script/wxr_export.rb

xml = Builder::XmlMarkup.new(:target =&gt; STDOUT, :indent =&gt; 2)

xml.instruct! :xml, :version=&gt;"1.0", :encoding=&gt;"UTF-8"

xml.rss 'version' =&gt; "2.0",
        'xmlns:content' =&gt; "http://purl.org/rss/1.0/modules/content/",
        'xmlns:dsq' =&gt; "http://www.disqus.com/",
        'xmlns:dc' =&gt; "http://purl.org/dc/elements/1.1/",
        'xmlns:wp' =&gt; "http://wordpress.org/export/1.0/" do
 
  xml.channel do
    Articles.all.each do |article|
      if should_be_exported?(article)
        xml.item do

          #Article XML goes here

   article.comments.each do |comment|
      
     #Comments XML goes here

   end #article.comments.each
 end   #xml.item
      end     #if should_be_exported?
    end       #Articles.all.each
  end         #xml.channel
end           #xml.rss
&lt;/pre&gt;&lt;p&gt;This is the general form for the &lt;a href="http://docs.disqus.com/developers/export/import_format/"&gt;WXR format&lt;/a&gt; as described by Disqus's knowledge base article. Note that you need to nest the comments inside each specific Article's XML. I found that I needed to filter some of my output so I added a helper function called &lt;code&gt;should_be_exported?&lt;/code&gt; which can be defined at the top of the script. This would allow you to exclude Articles without comments, or whatever criteria you might find helpful.&lt;/p&gt;&lt;p&gt;With our basic format in place, let's look at the syntax for exporting the Article fields. Keep in mind that the fields you'll want to pull from in your system will likely be different, but the intention is the same.&lt;/p&gt;&lt;h3&gt;Inside the Article XML block&lt;/h3&gt;&lt;pre&gt;# script/wxr_export.rb

# Inside the Article XML block

xml.title article.title

xml.link create_url_for(article)

xml.content(:encoded) { |x| x &lt;&lt; "&lt;![CDATA[" + article.body + "]]&gt;" }

xml.dsq(:thread_identifier) { |x| x &lt;&lt; article.id }

xml.wp(:post_date_gmt) { |x| x &lt;&lt; article.created_at.utc.to_formatted_s(:db) }

xml.wp(:comment_status) { |x| x &lt;&lt; "open" } #all comments open
&lt;/pre&gt;
&lt;p&gt;Let's look at each of these fields one by one:
&lt;ul&gt;&lt;li&gt;&lt;b&gt;xml.title&lt;/b&gt;: This is pretty straight forward, just the plain text tile of the blog article.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;xml.link&lt;/b&gt;: Disqus can use URLs for determining which comments to display on your page, so it asks you to provide a URL associated with this article.  I found that for this particular app, it would be easier to write another helper function to generate the URLs then using the Rails routes.  If you wish to use the Rails routes (and I suggest you do), then I suggest checking out this excellent post for &lt;a href="http://steve.dynedge.co.uk/2010/04/29/rails-3-rake-and-url_for/"&gt;using routes outside of views&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;xml.content(:encoded)&lt;/b&gt;: The purpose of this field is clear, but the syntax is not. Hope this saves you some time and headache!&lt;/li&gt;
&lt;li&gt;&lt;b&gt;xml.dsq(:thread_identifier)&lt;/b&gt;: The other way Disqus can identify your article is by a unique identifier. This is strongly recommended over the use of a URL. We'll just use your unique identifier in the database.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;xml.wp(:post_date_gmt)&lt;/b&gt;: The thing to keep in mind here is that we need the date in a very particular format. It needs to be in YYYY-MM-DD HH:MM:SS 24-hour format and adjusted to GMT which &lt;a href="http://en.wikipedia.org/wiki/Coordinated_Universal_Time#Definition_and_relationship_to_other_standards"&gt;typically implies UTC&lt;/a&gt;. Rails 3 makes this very easy for us, bless their hearts.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;xml.wp(:comment_status)&lt;/b&gt;: This app wanted to leave all comments open. You may have different requirements so consider adding a helper function.&lt;/li&gt;
&lt;/ul&gt;&lt;/p&gt;&lt;h3&gt;Inside the Comment XML block&lt;/h3&gt;&lt;pre&gt;article.comments.each do |comment|
  
  xml.wp(:comment) do

    xml.wp(:comment_id) { |x| x &lt;&lt; comment.id }

    xml.wp(:comment_author) do |x| 
      if comment.user.present? &amp;&amp; comment.user.name.present?
        x &lt;&lt; comment.user.name
      else
 x &lt;&lt; ""
      end 
    end 
                  
    xml.wp(:comment_author_email) do |x| 
      if comment.user.present? &amp;&amp; comment.user.email.present?
        x &lt;&lt; comment.user.email
      else
        x &lt;&lt; ""
      end 
    end 

    xml.wp(:comment_author_url) do |x|
      if comment.user.present? &amp;&amp; comment.user.url.present?
        x &lt;&lt; comment.user.url
      else
        x &lt;&lt; ""
      end
    end

    xml.wp(:comment_author_IP) { |x| x &lt;&lt; "255.255.255.255" }

    xml.wp(:comment_date_gmt) { |x| x &lt;&lt; comment.created_at.utc.to_formatted_s(:db) }

    xml.wp(:comment_content) { |x| x &lt;&lt; "&lt;![CDATA[" + comment.body + "]]&gt;" }

    xml.wp(:comment_approved) { |x| x &lt;&lt; 1 } #approve all comments

    xml.wp(:comment_parent) { |x| x &lt;&lt; 0 }

  end #xml.wp(:comment)
end #article.comments.each
&lt;/pre&gt;
&lt;p&gt;Again, let's inspect this one field at a time:
&lt;ul&gt;&lt;li&gt;&lt;b&gt;xml.wp(:comment_id)&lt;/b&gt;: Straightforward, a simple unique identifier for the comment.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;xml.wp(:comment_author)&lt;/b&gt;: Because some commentors may not have a user associated with them, I added some extra checks to make sure the author's user and name were present. I'm sure there's a way to shorten the number of lines used, but I was going for readability here. I'm not certain it was necessary to include the blank string, but after some of the trouble I had importing, I wanted to minimize the chance of strange XML syntax issues.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;xml.wp(:comment_author_email)&lt;/b&gt;: More of the same safe guards of having empty data.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;xml.wp(:comment_author_url)&lt;/b&gt;: More of the same safe guards of having empty data.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;xml.wp(:comment_author_IP)&lt;/b&gt;: We were not collecting user IP data, so I put in some bogus data which Disqus did not seem to mind.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;xml.wp(:comment_date_gmt)&lt;/b&gt;: See xml.wp(:post_date_gmt) above for comments about date/time format.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;xml.wp(:comment_content)&lt;/b&gt;: See xml.content(:encoded) above for comments about encoding content.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;xml.wp(:comment_approved)&lt;/b&gt;: Two options here, 0 or 1.  Typically you'd want to automatically approve your existing comments, unless of course you wanted to give a moderator a huge backlog of work.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;xml.wp(:comment_parent)&lt;/b&gt;: This little field turned out to be the cause of a lot of trouble for me.  In the comments on Disqus's XML example, it says &lt;code&gt;parent id (match up with wp:comment_id)&lt;/code&gt;, so initially, I just put in the comment's ID in this field.  This returned the very unhelpful error &lt;code&gt;* url * URL is required&lt;/code&gt; to which I still have my unanswered supprot email in to Disqus.  By trial error, I found that by just setting the comment_parent to zero, I could successfully upload my comment content.  If you are using threaded comments, I suspect this field will be of more importance to you then it was to me.  When I hear from Disqus, I will update this article with more information.&lt;/li&gt;
&lt;/ul&gt;&lt;/p&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/7997313029981170997-1503871732819562242?l=blog.endpoint.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://blog.endpoint.com/feeds/1503871732819562242/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=7997313029981170997&amp;postID=1503871732819562242' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/7997313029981170997/posts/default/1503871732819562242'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/7997313029981170997/posts/default/1503871732819562242'/><link rel='alternate' type='text/html' href='http://blog.endpoint.com/2011/12/importing-comments-into-disqus-using.html' title='Importing Comments into Disqus using Rails'/><author><name>Brian Buchalter</name><uri>http://www.blogger.com/profile/01591156911422086869</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='http://2.bp.blogspot.com/-O1WazcpsfX8/TwSI332IhgI/AAAAAAAAABo/UZzjB39UXJ4/s220/headshot_close.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-7997313029981170997.post-2328807301197381272</id><published>2011-12-23T10:24:00.000-05:00</published><updated>2011-12-23T13:25:04.018-05:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='javascript'/><category scheme='http://www.blogger.com/atom/ns#' term='tools'/><category scheme='http://www.blogger.com/atom/ns#' term='jquery'/><category scheme='http://www.blogger.com/atom/ns#' term='tips'/><title type='text'>Labeling input boxes including passwords</title><content type='html'>&lt;p&gt;I'm currently working on a new site and one of the design aspects of the site is many of the form fields do not have labels near the input boxes, they utilize labels that are inside the input box and fade away when text is entered.  The label is also supposed to reappear if the box is cleared out.  Originally I thought this was a pretty easy problem and wrote out some jQuery to do this quickly.  The path I went down first was to set the textbox to the value we wanted displayed and then clear it on focus.  This worked fine, however I reached a stumbling block when it came to password input boxes.  My solution did not work properly because text in a password box is hidden and the label would be hidden as well.  Most people would probably understand what went in each box, but I didn't want to risk confusing anyone, so I needed to find a better solution&lt;/p&gt;

&lt;p&gt;I did some searching for jQuery and labels for password inputs and turned up several solutions.  The first one actually put another text box on top of the password input, but that seemed prone to issue.  The solution I decided to ultimately use is called &lt;a href="http://fuelyourcoding.com/in-field-labels/" target="_blank"&gt;In-Fields Labels&lt;/a&gt;, a jQuery plugin by Doug Neiner. In this solution Doug has floating labels that appear over the top of the textbox, and they dim slightly when focus is gained and then disappear completely when typing begins.  The plugin does not mess with the value in the input box at all.&lt;/p&gt;

&lt;p&gt;It was fairly easy to get up and running.  I added the plugin to the page, created some styling for the labels, added label tags with the class of 'overlay' for each input box and called $('label.overlay').inFieldLabels();.  This was all that was needed to get us going.&lt;/p&gt;

&lt;p&gt;
&lt;i&gt;Normal view&lt;/i&gt;&lt;br&gt;
&lt;a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://1.bp.blogspot.com/-kcx9qFf4n0w/TvTFmuwAPfI/AAAAAAAAADA/l-7rNbreDfo/s1600/label_1.jpg"&gt;&lt;img style="cursor:pointer; cursor:hand;width: 370px; height: 230px;" src="http://1.bp.blogspot.com/-kcx9qFf4n0w/TvTFmuwAPfI/AAAAAAAAADA/l-7rNbreDfo/s400/label_1.jpg" border="0" alt=""id="BLOGGER_PHOTO_ID_5689389498450853362" /&gt;&lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;
&lt;i&gt;Focus in the password box&lt;/i&gt;&lt;br&gt;
&lt;a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://3.bp.blogspot.com/-iYmeVuxNTLs/TvTFm_WJmsI/AAAAAAAAADI/6oFF7Ya5mP0/s1600/label_2.jpg"&gt;&lt;img style="cursor:pointer; cursor:hand;width: 370px; height: 230px;" src="http://3.bp.blogspot.com/-iYmeVuxNTLs/TvTFm_WJmsI/AAAAAAAAADI/6oFF7Ya5mP0/s400/label_2.jpg" border="0" alt=""id="BLOGGER_PHOTO_ID_5689389502905817794" /&gt;&lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;
&lt;i&gt;Typing in the password box&lt;/i&gt;&lt;br&gt;
&lt;a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://4.bp.blogspot.com/-_0xclWFaS8k/TvTFm3B2YGI/AAAAAAAAADQ/tjvdz8-419k/s1600/label_3.jpg"&gt;&lt;img style="cursor:pointer; cursor:hand;width: 372px; height: 230px;" src="http://4.bp.blogspot.com/-_0xclWFaS8k/TvTFm3B2YGI/AAAAAAAAADQ/tjvdz8-419k/s400/label_3.jpg" border="0" alt=""id="BLOGGER_PHOTO_ID_5689389500673187938" /&gt;&lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;The effect is pretty cool and it provides a good interface for the user as they are reminded up until the time they type in the box what they are supposed to enter.&lt;/p&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/7997313029981170997-2328807301197381272?l=blog.endpoint.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://blog.endpoint.com/feeds/2328807301197381272/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=7997313029981170997&amp;postID=2328807301197381272' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/7997313029981170997/posts/default/2328807301197381272'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/7997313029981170997/posts/default/2328807301197381272'/><link rel='alternate' type='text/html' href='http://blog.endpoint.com/2011/12/labeling-input-boxes-including.html' title='Labeling input boxes including passwords'/><author><name>Ron Phipps</name><uri>http://www.blogger.com/profile/02106747784162431265</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='23' src='http://bp1.blogger.com/_qTrL2g2nY1M/SHFY1QqZ-YI/AAAAAAAAAAM/T6MCGHloJXU/S220/img_ron_240.jpg'/></author><media:thumbnail xmlns:media='http://search.yahoo.com/mrss/' url='http://1.bp.blogspot.com/-kcx9qFf4n0w/TvTFmuwAPfI/AAAAAAAAADA/l-7rNbreDfo/s72-c/label_1.jpg' height='72' width='72'/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-7997313029981170997.post-2666459910971123837</id><published>2011-12-22T18:57:00.004-05:00</published><updated>2011-12-22T19:19:34.760-05:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='redhat'/><category scheme='http://www.blogger.com/atom/ns#' term='open-source'/><category scheme='http://www.blogger.com/atom/ns#' term='tips'/><category scheme='http://www.blogger.com/atom/ns#' term='sysadmin'/><category scheme='http://www.blogger.com/atom/ns#' term='hosting'/><title type='text'>Converting CentOS 6 to RHEL 6</title><content type='html'>&lt;p&gt;A few years ago I needed to convert a &lt;a href="http://www.redhat.com/rhel/"&gt;Red Hat Enterprise Linux&lt;/a&gt; (RHEL) 5 development system to &lt;a href="http://www.centos.org/"&gt;CentOS&lt;/a&gt; 5, as our customer did not actively use the system any more and no longer wanted to renew the Red Hat Network entitlement for it. Making the conversion was surprisingly straightforward.&lt;/p&gt;

&lt;p&gt;This week I needed to make a conversion in the opposite direction: from CentOS 6 to RHEL 6. I didn't find any instructions on doing so, but found a &lt;a href="http://ivo.livejournal.com/75008.html"&gt;RHEL 6 to CentOS 6 conversion guide&lt;/a&gt; with roughly these steps:&lt;/p&gt;

&lt;pre&gt;yum clean all
mkdir centos
cd centos
wget http://mirror.centos.org/centos/6.0/os/x86_64/RPM-GPG-KEY-CentOS-6
wget http://mirror.centos.org/centos/6.0/os/x86_64/Packages/centos-release-6-0.el6.centos.5.x86_64.rpm
wget http://mirror.centos.org/centos/6.0/os/x86_64/Packages/yum-3.2.27-14.el6.centos.noarch.rpm
wget http://mirror.centos.org/centos/6.0/os/x86_64/Packages/yum-utils-1.1.26-11.el6.noarch.rpm
wget http://mirror.centos.org/centos/6.0/os/x86_64/Packages/yum-plugin-fastestmirror-1.1.26-11.el6.noarch.rpm
rpm --import RPM-GPG-KEY-CentOS-6
rpm -e --nodeps redhat-release-server
rpm -e yum-rhn-plugin rhn-check rhnsd rhn-setup rhn-setup-gnome
rpm -Uhv --force *.rpm
yum upgrade&lt;/pre&gt;

&lt;p&gt;I then put together a plan to do more or less the opposite of that. The high-level overview of the steps is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Completely upgrade the current CentOS and reboot to run the latest kernel, if necessary, to make sure you're starting with a solid system.&lt;/li&gt;
&lt;li&gt;Install a handful of packages that will be needed by various RHN tools.&lt;/li&gt;
&lt;li&gt;Log into the Red Hat Network web interface and search for and download onto the server the most recent version of these packages for RHEL 6 x86_64:
&lt;ul&gt;
&lt;li&gt;redhat-release-server-6Server&lt;/li&gt;
&lt;li&gt;rhn-check&lt;/li&gt;
&lt;li&gt;rhn-client-tools&lt;/li&gt;
&lt;li&gt;rhnlib&lt;/li&gt;
&lt;li&gt;rhnsd&lt;/li&gt;
&lt;li&gt;rhn-setup&lt;/li&gt;
&lt;li&gt;yum&lt;/li&gt;
&lt;li&gt;yum-metadata-parser&lt;/li&gt;
&lt;li&gt;yum-rhn-plugin&lt;/li&gt;
&lt;li&gt;yum-utils&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Install the Red Hat GnuPG signing key.&lt;/li&gt;
&lt;li&gt;Forcibly remove the package that identifies this system as CentOS.&lt;/li&gt;
&lt;li&gt;Forcibly upgrade to the downloaded RHEL and RHN packages.&lt;/li&gt;
&lt;li&gt;Register the system with Red Hat Network.&lt;/li&gt;
&lt;li&gt;Update any packages that now need it using the new Yum repository.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The exact steps I used today to convert from CentOS 6.1 to RHEL 6.2 (with URL session tokens munged):&lt;/p&gt;

&lt;pre&gt;
yum upgrade
shutdown -r now
yum install dbus-python libxml2-python m2crypto pyOpenSSL python-dmidecode python-ethtool python-gudev usermode
mkdir rhel
cd rhel
wget 'https://content-web.rhn.redhat.com/rhn/public/NULL/redhat-release-server/6Server-6.2.0.3.el6/x86_64/redhat-release-server-6Server-6.2.0.3.el6.x86_64.rpm?__gda__=XXX_YYY&amp;ext=.rpm'
wget 'https://content-web.rhn.redhat.com/rhn/public/NULL/rhn-check/1.0.0-73.el6/noarch/rhn-check-1.0.0-73.el6.noarch.rpm?__gda__=XXX_YYY&amp;ext=.rpm'
wget 'https://content-web.rhn.redhat.com/rhn/public/NULL/rhn-client-tools/1.0.0-73.el6/noarch/rhn-client-tools-1.0.0-73.el6.noarch.rpm?__gda__=XXX_YYY&amp;ext=.rpm'
wget 'https://content-web.rhn.redhat.com/rhn/public/NULL/rhnlib/2.5.22-12.el6/noarch/rhnlib-2.5.22-12.el6.noarch.rpm?__gda__=XXX_YYY&amp;ext=.rpm'
wget 'https://content-web.rhn.redhat.com/rhn/public/NULL/rhnsd/4.9.3-2.el6/x86_64/rhnsd-4.9.3-2.el6.x86_64.rpm?__gda__=XXX_YYY&amp;ext=.rpm'
wget 'https://content-web.rhn.redhat.com/rhn/public/NULL/rhn-setup/1.0.0-73.el6/noarch/rhn-setup-1.0.0-73.el6.noarch.rpm?__gda__=XXX_YYY&amp;ext=.rpm'
wget 'https://content-web.rhn.redhat.com/rhn/public/NULL/yum/3.2.29-22.el6/noarch/yum-3.2.29-22.el6.noarch.rpm?__gda__=XXX_YYY&amp;ext=.rpm'
wget 'https://content-web.rhn.redhat.com/rhn/public/NULL/yum-metadata-parser/1.1.2-16.el6/x86_64/yum-metadata-parser-1.1.2-16.el6.x86_64.rpm?__gda__=XXX_YYY&amp;ext=.rpm'
wget 'https://content-web.rhn.redhat.com/rhn/public/NULL/yum-rhn-plugin/0.9.1-36.el6/noarch/yum-rhn-plugin-0.9.1-36.el6.noarch.rpm?__gda__=XXX_YYY&amp;ext=.rpm'
wget 'https://content-web.rhn.redhat.com/rhn/public/NULL/yum-utils/1.1.30-10.el6/noarch/yum-utils-1.1.30-10.el6.noarch.rpm?__gda__=XXX_YYY&amp;ext=.rpm'
wget https://www.redhat.com/security/fd431d51.txt
rpm --import fd431d51.txt
rpm -e --nodeps centos-release
rpm -e centos-release-cr
rpm -Uhv --force *.rpm
rpm -e yum-plugin-fastestmirror
yum clean all
rhn_register
yum upgrade
&lt;/pre&gt;

&lt;p&gt;I'm expecting to use this process a few more times in the near future. It is very useful when working with a hosting provider that does not directly support RHEL, but provides CentOS, so we can get the new servers set up without needing to request a custom operating system installation that may add a day or two to the setup time.&lt;/p&gt;

&lt;p&gt;Given the popularity of both RHEL and CentOS, it would be neat for Red Hat to provide a tool that would easily switch, at least "upgrading" from CentOS to RHEL to bring more customers into their fold, if not the other direction!&lt;/p&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/7997313029981170997-2666459910971123837?l=blog.endpoint.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://blog.endpoint.com/feeds/2666459910971123837/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=7997313029981170997&amp;postID=2666459910971123837' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/7997313029981170997/posts/default/2666459910971123837'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/7997313029981170997/posts/default/2666459910971123837'/><link rel='alternate' type='text/html' href='http://blog.endpoint.com/2011/12/converting-centos-6-to-rhel-6.html' title='Converting CentOS 6 to RHEL 6'/><author><name>Jon Jensen</name><uri>http://www.blogger.com/profile/18273388885281263476</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='25' src='http://bp3.blogger.com/_rFXHDrokbpE/SJHpPosaIQI/AAAAAAAAAAM/GnqeZuLItOA/S220/jon1.png'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-7997313029981170997.post-2340208877454627200</id><published>2011-12-21T13:41:00.007-05:00</published><updated>2011-12-23T13:31:59.215-05:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='ruby'/><category scheme='http://www.blogger.com/atom/ns#' term='open-source'/><category scheme='http://www.blogger.com/atom/ns#' term='Spree'/><category scheme='http://www.blogger.com/atom/ns#' term='ecommerce'/><category scheme='http://www.blogger.com/atom/ns#' term='ruby-and-ruby-on-rails'/><title type='text'>Rails Request-Based Routing Constraints in Spree</title><content type='html'>&lt;p&gt;I recently adopted an unreleased ecommerce project running Spree 0.60.0 on Rails 3.0.9. The site used a Rails routing constraint and wildcard DNS to dynamically route subdomains to the “dispatch” action of the organizations_controller. If a request’s subdomain component matched that regular expression, it was routed to the dispatch method. Here's the original route:&lt;/p&gt;

&lt;pre&gt;
match '/' =&gt; 'organizations#dispatch', :constraints =&gt; { :subdomain =&gt; /.+/ }
&lt;/pre&gt;

&lt;p&gt;The business requirement driving this feature was that a User could register an Organization by submitting a form on the site. Once that Organization was marked "approved" by an admin, that Organization would become accessible at their own subdomain - &lt;em&gt;no server configuration required&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;For marketing reasons, we decided to switch from subdomains to top-level subdirectories. This meant RESTful routes (e.g. domain.com/organizations/143) wouldn’t cut it. In order to handle this, I created a routing constraint class called OrgConstraint. This routing constraint class works in tandem with a tweaked version of that original route.&lt;/p&gt;

&lt;pre&gt;
match '*org_url' =&gt; 'organizations#show', :constraints =&gt; OrgConstraint.new
&lt;/pre&gt;

&lt;p&gt;The :constraints param takes an &lt;em&gt;instance&lt;/em&gt; of a class (not a class name) that responds to a matches? predicate method that returns true or false. If matches? returns true, the request will be routed to that controller#action. Else, that route is treated like any other non-matching route. Here’s the entire OrgConstraint class:&lt;/p&gt;

&lt;pre&gt;
class OrgConstraint
  def matches?(request)
    Organization.valid_url? request.path_parameters[:org_url]
  end
end
&lt;/pre&gt;

&lt;p&gt;Note how Rails automatically passes the &lt;a href="http://guides.rubyonrails.org/action_controller_overview.html#the-request-object"&gt;request object&lt;/a&gt; to the matches? method. Also note how the relative url of the request is available via the :org_url symbol - the same identifier we used in the route definition. The Organization.valid_url? class method encapsulates the logic of examining a simple cached (via Rails.cache) hash consisting of organization urls as keys and true as their value.&lt;/p&gt;

&lt;p&gt;The final step in this process is, of course, the organizations_controller’s show method. It now needs to look for that same :org_url param that the route definition creates, in the standard params hash we all know and love:&lt;/p&gt;

&lt;pre&gt;
def show
  @organization = Organization.find(params[:id]) if params[:id]  
  # from routing constraint
  @organization ||= Organization.find_by_url(params[:org_url]) if params[:org_url]  
  ...
end
&lt;/pre&gt;

&lt;p&gt;I should point out that Rails instantiates exactly one instance of your routing constraint class when it first loads your routes. This means you’ll want to ensure your class’s design will respond appropriately to changes in any underlying data. This is one of the reasons the Organization class caches the {$org_url =&gt; true} hash rather than using instance variables within the OrgConstraint class.&lt;/p&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/7997313029981170997-2340208877454627200?l=blog.endpoint.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://blog.endpoint.com/feeds/2340208877454627200/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=7997313029981170997&amp;postID=2340208877454627200' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/7997313029981170997/posts/default/2340208877454627200'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/7997313029981170997/posts/default/2340208877454627200'/><link rel='alternate' type='text/html' href='http://blog.endpoint.com/2011/12/rails-spree-routing-constraint.html' title='Rails Request-Based Routing Constraints in Spree'/><author><name>Brian Gadoury</name><uri>http://www.blogger.com/profile/05029645301649082254</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-7997313029981170997.post-8748396070146324534</id><published>2011-12-20T07:00:00.001-05:00</published><updated>2011-12-20T07:00:10.175-05:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='ruby'/><category scheme='http://www.blogger.com/atom/ns#' term='ruby-and-ruby-on-rails'/><title type='text'>Modifying Models in Rails Migrations</title><content type='html'>&lt;p&gt;
As migrations have piled up in projects that I work on, one problem seems to come up fairly consistently. New changes to models can break migrations.
&lt;/p&gt;

&lt;p&gt;
This can happen a number of different ways. One way is to break old migrations. Another is for the changes to be made to the file before the migration is run (timing issues with version control).
&lt;/p&gt;

&lt;p&gt;
While these can be (and usually are) considered coordination rather than technical issues, sometimes you just need to handle them and move on.
&lt;/p&gt;

&lt;p&gt;
One case I'd like to cover here is removing or changing associations.  At the time the migration is expected to run, the file for the model class will have been updated already, so it is hard use that in the migration itself, even though it would be useful.
&lt;/p&gt;

&lt;p&gt;
In this case I found myself with an even slightly trickier example.  I have a model that contains some address info.  Part of that is an association to an external table that lists the states. So part of the
class definition was like so:
&lt;/p&gt;

&lt;pre&gt;
Class Contact 
 belongs_to :state
 ...
end
&lt;/pre&gt;

&lt;p&gt;
What I needed to do in the migration was to remove the association and
introduce another field called "state" which would just be a varchar
field representing the state part of the address.  The two problems the
migration would encounter were:
&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The state association would not exist at the time it ran&lt;/li&gt;
&lt;li&gt;And even if it did, there would be a name conflict between it and the
new column I wanted&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;
To get around these restrictions I did this in my migration:
&lt;/p&gt;

&lt;pre&gt;
Contact.class_eval do
  belongs_to :orig_state,
             :class_name =&gt; "State",
             :foreign_key =&gt; "state_id"
end
&lt;/pre&gt;
&lt;p&gt;
This creates a different association named "orig_state" using the states table for the Contact class. I can now use my original migration code more-or-less as is, and still create a new state column.
column.
&lt;/p&gt;

&lt;p&gt;
Another problem I had was that the table had about 300 rows of data that
failed one of the validations called "validate_names".  I didn't feel
like sorting it out, so I just added the following code to the above
class_eval block:
&lt;/p&gt;

&lt;pre&gt;
define_method(:validate_names) do
  true
end
&lt;/pre&gt;

&lt;p&gt;
With these two modifications to the Contact class, I was able to use the simple migration with all of my Rails associations to do what I needed in the migration without resorting to hand crafting more complex SQL that would have been required in order to not have to refer to the model classes at all in the migration.
&lt;/p&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/7997313029981170997-8748396070146324534?l=blog.endpoint.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://blog.endpoint.com/feeds/8748396070146324534/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=7997313029981170997&amp;postID=8748396070146324534' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/7997313029981170997/posts/default/8748396070146324534'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/7997313029981170997/posts/default/8748396070146324534'/><link rel='alternate' type='text/html' href='http://blog.endpoint.com/2011/12/modifying-models-in-rails-migrations.html' title='Modifying Models in Rails Migrations'/><author><name>Sonny Cook</name><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-7997313029981170997.post-5958776765672361329</id><published>2011-12-19T11:05:00.000-05:00</published><updated>2011-12-19T11:11:27.736-05:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='javascript'/><category scheme='http://www.blogger.com/atom/ns#' term='jquery'/><category scheme='http://www.blogger.com/atom/ns#' term='ruby-and-ruby-on-rails'/><title type='text'>Nifty In-Button Confirmation</title><content type='html'>&lt;p&gt;I've been working on a personal email client after work, called &lt;a href="http://warmsunrise.com"&gt;Warm Sunrise&lt;/a&gt;, that forces myself to keep a manageable inbox. One of the goals of the project was to get to a zero-inbox everyday, so I needed a 'Delete All' button that was easy-to-use without running the risk of &lt;em&gt;accidentally&lt;/em&gt; deleting emails. I took a look at JavaScript's confirm, which is jarring, and jQuery's dblClick, which doesn't provide any feedback to the user after the first click, leaving the user to wonder why their emails weren't deleted.&lt;/p&gt;&lt;p&gt;Given these options, I built my own button using Rails 3.1, jQuery, and CoffeeScript, that better fit the goals I set out with. It requires a double click, but gives the user a confirmation in the button itself, without any sort of timeout. You can see a video of it in action here:&lt;/p&gt;&lt;br /&gt;
&lt;p&gt;&lt;iframe frameborder="0" height="360" src="http://www.youtube-nocookie.com/embed/diKdYAW0r88" width="640"&gt;&lt;/iframe&gt;&lt;/p&gt;&lt;p&gt;Starting with &lt;strong&gt;app/views/letters/index.html.erb&lt;/strong&gt;, I generated the buttons using Rails helpers and Twitter's Bootstrap classes:&lt;/p&gt;&lt;pre class="brush: ruby"&gt;&amp;lt;%= link_to 'Write letter', new_letter_path, :class =&amp;gt; "btn primary pull-right far-right" %&amp;gt;
&amp;lt;%= link_to 'Delete all', '#', :class =&amp;gt; "btn pull-right no_danger", :id =&amp;gt; "delete_all" %&amp;gt;
&amp;lt;%= link_to 'Are you sure?', delete_all_letters_path, :method =&amp;gt; :destroy, :class =&amp;gt;"btn pull-right danger confirm", :id =&amp;gt; "delete_all", :style =&amp;gt; "display:none;" %&amp;gt;&lt;/pre&gt;&lt;p&gt;Notice that the 'Delete all' button doesn't actually specify a url and the 'Are you sure?' link's style is set to "display:none"&lt;/p&gt;&lt;p&gt;Here's the relationship I set up in my models:&lt;/p&gt;&lt;p&gt;&lt;strong&gt;app/models/letter.rb&lt;/strong&gt;&lt;/p&gt;&lt;pre class="brush: ruby"&gt;belongs to :user&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;app/models/user.rb&lt;/strong&gt;&lt;/p&gt;&lt;pre class="brush: ruby"&gt;has_many :letters, :dependent =&amp;gt; :destroy&lt;/pre&gt;&lt;p&gt;I set up &lt;strong&gt;config/routes.rb&lt;/strong&gt; to work with the explicit path I set in:&lt;/p&gt;&lt;pre class="brush: ruby"&gt;post 'delete_all_letters' =&amp;gt; 'letters#delete_all'&lt;/pre&gt;&lt;p&gt;Finally, I finished this lot by adding the delete_all action to my &lt;strong&gt;app/controllers/letters_controller.rb&lt;/strong&gt;:&lt;/p&gt;&lt;pre class="brush: ruby"&gt;def delete_all 
    current_user.letters.delete_all

    respond_to do |format|
        format.html { redirect_to letters_url, notice: 'Successfully deleted all letters.' }
        format.json { head :ok }
    end 
end 
&lt;/pre&gt;&lt;p&gt;CoffeeScript is a beautiful language that compiles to JavaScript, which I prefer to JavaScript itself. You can read more about it &lt;a href="http://jashkenas.github.com/coffee-script/"&gt;here&lt;/a&gt;. Let's take a look at the &lt;b&gt;CoffeeScript&lt;/b&gt; that makes this button work:&lt;/p&gt;&lt;pre class="brush: js"&gt;$('a#delete_all.no_danger').hover( -&amp;gt;
    $(this).addClass('danger')
    $(this).click( -&amp;gt;
        $('a#delete_all.no_danger').hide()
        $('a#delete_all.confirm').show()
    )   
)
$('a#delete_all.no_danger').mouseleave( -&amp;gt;
    $(this).removeClass('danger')
)
$('a#delete_all.danger').mouseleave( -&amp;gt;
    $(this).hide()
    $('a#delete_all.no_danger').show()
)
&lt;/pre&gt;&lt;p&gt;Since the button's text changes to a confirmation on the first click, makes it better for my purposes than Javascript's dblClick method. Check the video to see what it looks like in action.&lt;/p&gt;&lt;p&gt;Let's take a look at what this compiles to in plain &lt;strong&gt;JavaScript&lt;/strong&gt;, too, since this is the only thing the browser sees:&lt;/p&gt;&lt;pre class="brush: js"&gt;$('a#delete_all.no_danger').hover(function() {
    $(this).addClass('danger');
    return $(this).click(function() {
        $('a#delete_all.no_danger').hide();
        return $('a#delete_all.confirm').show();
    });
});
$('a#delete_all.no_danger').mouseleave(function() {
    return $(this).removeClass('danger');
});
$('a#delete_all.danger').mouseleave(function() {
    $(this).hide();
    return $('a#delete_all.no_danger').show();
});&lt;/pre&gt;&lt;p&gt;Not shown in the video, but I modified index.html.erb to only show the 'Delete all' button when the user has a zero-inbox.&lt;/p&gt;&lt;pre class="brush: ruby"&gt;&amp;lt;%= link_to 'Write letter', new_letter_path, :class =&amp;gt; "btn primary pull-right far-right" %&amp;gt;
&amp;lt;% if !@letters.empty? %&amp;gt;
    &amp;lt;%= link_to 'Delete all', '#', :class =&amp;gt; "btn pull-right no_danger", :id =&amp;gt; "delete_all" %&amp;gt;
    &amp;lt;%= link_to 'Are you sure?', delete_all_letters_path, :method =&amp;gt; :destroy, :class =&amp;gt;"btn pull-right danger confirm", :id =&amp;gt; "delete_all", :style =&amp;gt; "display:none;" %&amp;gt;
&amp;lt;% end %&amp;gt;&lt;/pre&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/7997313029981170997-5958776765672361329?l=blog.endpoint.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://blog.endpoint.com/feeds/5958776765672361329/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=7997313029981170997&amp;postID=5958776765672361329' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/7997313029981170997/posts/default/5958776765672361329'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/7997313029981170997/posts/default/5958776765672361329'/><link rel='alternate' type='text/html' href='http://blog.endpoint.com/2011/12/nifty-in-button-confirmation.html' title='Nifty In-Button Confirmation'/><author><name>Evan Tann</name><uri>https://profiles.google.com/106289380456101697641</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='//lh4.googleusercontent.com/-kSHNiNmGQFI/AAAAAAAAAAI/AAAAAAAAAEw/jhF16LMC5fw/s512-c/photo.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-7997313029981170997.post-5805577856505668568</id><published>2011-12-17T10:35:00.004-05:00</published><updated>2011-12-17T11:58:30.440-05:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='tools'/><category scheme='http://www.blogger.com/atom/ns#' term='unicode'/><category scheme='http://www.blogger.com/atom/ns#' term='tips'/><category scheme='http://www.blogger.com/atom/ns#' term='perl'/><category scheme='http://www.blogger.com/atom/ns#' term='postgres'/><title type='text'>Sanitizing supposed UTF-8 data</title><content type='html'>&lt;p&gt;As time passes, it's clear that Unicode has won the character set encoding wars, and UTF-8 is by far the most popular encoding, and the expected default. In a few more years we'll probably find discussion of different character set encodings to be arcane, relegated to "data historians" and people working with legacy systems.&lt;/p&gt;

&lt;p&gt;But we're not there yet! There's still lots of migration to do before we can forget about everything that's not UTF-8.&lt;/p&gt;

&lt;p&gt;Last week I again found myself converting data. This time I was taking data from a PostgreSQL database with no specified encoding (so-called "SQL_ASCII", really just raw bytes), and sending it via JSON to a remote web service. JSON uses UTF-8 by default, and that's what I needed here. Most of the source data was in either UTF-8, ISO Latin-1, or Windows-1252, but some was in non-Unicode Chinese or Japanese encodings, and some was just plain mangled.&lt;/p&gt;

&lt;p&gt;At this point I need to remind you about one of the most unusual aspects of UTF-8: It has limited valid forms. Legacy encodings typically used all or most of the 255 code points in their 8-byte space (leaving point 0 for traditional ASCII NUL). While UTF-8 is compatible with 7-bit ASCII, it does not allow any possible 8-bit byte in any position. See &lt;a href="http://en.wikipedia.org/wiki/UTF-8#Invalid_byte_sequences"&gt;the Wikipedia summary of invalid byte sequences&lt;/a&gt; to know what can be considered invalid.&lt;/p&gt;

&lt;p&gt;We had no need to try to fix the truly broken data, but we wanted to convert everything possible to UTF-8 and at the very least guarantee no invalid UTF-8 strings appeared in what we sent.&lt;/p&gt;

&lt;p&gt;I previously wrote about &lt;a href="http://blog.endpoint.com/2010/03/postgresql-utf-8-conversion.html"&gt;converting a PostgreSQL database dump to UTF-8&lt;/a&gt;, and used the Perl CPAN module &lt;a href="http://search.cpan.org/perldoc?IsUTF8"&gt;IsUTF8&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I was going to use that again, but looked around and found an even better module, exactly targeting this use case: &lt;a href="http://search.cpan.org/dist/Encoding-FixLatin/"&gt;Encoding::FixLatin&lt;/a&gt;, by Grant McLean. Its documentation says it "takes mixed encoding input and produces UTF-8 output" and that's exactly what it does, focusing on input with mixed UTF-8, Latin-1, and Windows-1252.&lt;/p&gt;

&lt;p&gt;It worked as advertised, very well. We would need to use a different module to convert some other legacy encodings, but in this case this was good enough and got the vast majority of the data right.&lt;/p&gt;

&lt;p&gt;There's even a standalone &lt;a href="http://search.cpan.org/dist/Encoding-FixLatin/script/fix_latin"&gt;fix_latin&lt;/a&gt; program designed specifically for processing Postgres pg_dump output from legacy encodings, with some nice examples of how to use it.&lt;/p&gt;

&lt;p&gt;One gotcha is similar to a catch that David Christensen reported with the Encode module in a &lt;a href="http://blog.endpoint.com/2010/12/character-encoding-in-perl-decodeutf8.html"&gt;blog post here about a year ago&lt;/a&gt;: If the Perl string already has the UTF-8 flag set, Encoding::FixLatin immediately returns it, rather than trying to process it. So it's important that the incoming data be a pure byte stream, or that you otherwise turn off the UTF-8 flag, if you expect it to change anything.&lt;/p&gt;

&lt;p&gt;Along the way I found some other CPAN modules that look useful for cases where I need more manual control than Encoding::FixLatin gives:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="http://search.cpan.org/perldoc?Search::Tools::UTF8"&gt;Search::Tools::UTF8&lt;/a&gt; - test for and/or fix bad ASCII, Latin-1, Windows-1252, and UTF-8 strings&lt;/li&gt;
&lt;li&gt;&lt;a href="http://search.cpan.org/perldoc?Encode::Detect"&gt;Encode::Detect&lt;/a&gt; - use Mozilla's universal charset detector and convert to UTF-8&lt;/li&gt;
&lt;li&gt;&lt;a href="http://search.cpan.org/perldoc?Unicode::Tussle"&gt;Unicode::Tussle&lt;/a&gt; - ridiculously comprehensive set of Unicode tools that has to be seen to be believed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once again Perl's thriving open source/free software community made my day!&lt;/p&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/7997313029981170997-5805577856505668568?l=blog.endpoint.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://blog.endpoint.com/feeds/5805577856505668568/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=7997313029981170997&amp;postID=5805577856505668568' title='3 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/7997313029981170997/posts/default/5805577856505668568'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/7997313029981170997/posts/default/5805577856505668568'/><link rel='alternate' type='text/html' href='http://blog.endpoint.com/2011/12/sanitizing-supposed-utf-8-data.html' title='Sanitizing supposed UTF-8 data'/><author><name>Jon Jensen</name><uri>http://www.blogger.com/profile/18273388885281263476</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='25' src='http://bp3.blogger.com/_rFXHDrokbpE/SJHpPosaIQI/AAAAAAAAAAM/GnqeZuLItOA/S220/jon1.png'/></author><thr:total>3</thr:total></entry><entry><id>tag:blogger.com,1999:blog-7997313029981170997.post-9161279180785037691</id><published>2011-12-15T12:31:00.002-05:00</published><updated>2011-12-15T12:37:40.208-05:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='git'/><category scheme='http://www.blogger.com/atom/ns#' term='monitoring'/><category scheme='http://www.blogger.com/atom/ns#' term='configuration-management'/><title type='text'>Hurray for tracking configuration files in source control</title><content type='html'>&lt;p&gt;In a number of places we've started tracking configuration files in git.  It's great for Postgres configs, Apache or nginx, DNS zone files, Nagios, all kinds of things.  A few clients have private offsite repos we push to, like at GitHub, but for the most part they're independent repos.  It's still great for keeping track of what was changed when, and by whom.&lt;/p&gt;&lt;p&gt;In one case we have a centralized Nagios instance that does little more than receive passive checks from a number of remote systems.  I'd set the checks on the remote systems but not loaded that configuration in yet.  However while getting the central system set up, muscle memory kicked in and I suddenly had a half-red console as it's loading in stale data.&lt;/p&gt;&lt;p&gt;We don't need a flood of false alerts over email, but I don't want to completely revert the config and lose all those services...&lt;/p&gt;&lt;pre&gt;[root nagios]# git stash; service nagios restart; git stash apply
Saved working directory and index state WIP on master: 0e9113b Made up commit for blog
HEAD is now at 0e9113b Made up commit for blog
Running configuration check...done.
Stopping nagios: .done.
Starting nagios: done.
# On branch master
# (etc)&lt;/pre&gt;&lt;p&gt;Green!  A small victory, for sure, but it shows one more advantage of modern SCM's.&lt;/p&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/7997313029981170997-9161279180785037691?l=blog.endpoint.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://blog.endpoint.com/feeds/9161279180785037691/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=7997313029981170997&amp;postID=9161279180785037691' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/7997313029981170997/posts/default/9161279180785037691'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/7997313029981170997/posts/default/9161279180785037691'/><link rel='alternate' type='text/html' href='http://blog.endpoint.com/2011/12/hurray-for-tracking-configuration-files.html' title='Hurray for tracking configuration files in source control'/><author><name>Josh Williams</name><uri>http://www.blogger.com/profile/10667170385197485182</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='26' height='32' src='http://1.bp.blogspot.com/_TkzEDzlZHMg/S9Cz1qScO5I/AAAAAAAAAAM/_Ukoe431U5w/s1600-R/williams.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-7997313029981170997.post-7437036873655373853</id><published>2011-12-15T10:02:00.016-05:00</published><updated>2011-12-15T16:06:27.179-05:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='javascript'/><category scheme='http://www.blogger.com/atom/ns#' term='yui'/><category scheme='http://www.blogger.com/atom/ns#' term='sinatra'/><title type='text'>Preventing Global Variables in JavaScript</title><content type='html'>&lt;div style="text-align:center;"&gt;&lt;i&gt;"JavaScript's biggest problem is its dependence on global variables"&lt;/i&gt;&lt;/div&gt;&lt;div style="text-align:center;"&gt;--Douglas Crockford, &lt;i&gt;JavaScript: The Good Parts&lt;/i&gt;&lt;/div&gt;&lt;p&gt;Recently I built out support for affiliate management into &lt;a href="http://locateexpress.com/"&gt;LocateExpress.com’s&lt;/a&gt; Sinatra app using JavaScript and YUI.&lt;/p&gt;&lt;a href="http://4.bp.blogspot.com/-9Yv0wDxD4xg/TuoOhUECaLI/AAAAAAAAALQ/dCGHdhrCE2o/s1600/Screen%2BShot%2B2011-12-15%2Bat%2B10.00.28%2BAM%2Bcopy.png"&gt;&lt;img style="display:block; margin:0px auto 10px; text-align:center;cursor:pointer; cursor:hand;width: 320px; height: 318px;" src="http://4.bp.blogspot.com/-9Yv0wDxD4xg/TuoOhUECaLI/AAAAAAAAALQ/dCGHdhrCE2o/s320/Screen%2BShot%2B2011-12-15%2Bat%2B10.00.28%2BAM%2Bcopy.png" border="0" alt=""id="BLOGGER_PHOTO_ID_5686373444992788658" /&gt;&lt;/a&gt; &lt;p&gt;I used a working page from the admin, Service Providers, as a starting point to get something up and running for affiliates quickly. By the time I finished, the Affiliates page worked great, but forms on the Service Provider page no longer populated with data.&lt;/p&gt;&lt;h3&gt;&lt;b&gt;Identifying a misbehaving global variable&lt;/b&gt;&lt;/h3&gt;&lt;p&gt;There were no errors in the console, and the forms on the Service Providers page remained broken even after restoring an old copy of service_providers.js. As it turns out, a global variable, edit_map, was defined within service_providers.js, and again in the copied affiliates.js. Credit for spotting the problem goes to Brian Miller.&lt;/p&gt;&lt;p&gt;The fix was as simple as moving edit_map's declaration into the file's YUI sandbox, so that version of edit_map wouldn't be visible to any other pages in the admin.&lt;/p&gt;&lt;h3&gt;&lt;b&gt;Preventing global variables&lt;/b&gt;&lt;/h3&gt;&lt;p&gt;As projects grow and complexity increases, it becomes easier and easier to overlook global variables and thus run into this tough-to-debug problem. Douglas Crockford’s Javascript: The Good Parts covers several workarounds to using global variables.&lt;/p&gt;&lt;p&gt;Rather than declaring variables globally, like this:&lt;/p&gt;&lt;pre&gt;var edit_map = { 'business[name]' : 'business_name' };&lt;/pre&gt;&lt;p&gt;the author recommends declaring them at the beginning of functions whenever possible:&lt;/p&gt;&lt;pre&gt;YUI().use("node", "io", "json",
function(Y) {
    var edit_map = { 'business[name]' : 'business_name' };
    ...
});&lt;/pre&gt;&lt;p&gt;In all other cases, he suggests using Global Abatement, which prevents your global variables from affecting other libraries. For example,&lt;/p&gt;&lt;pre&gt;var LocateExpress = {};
LocateExpress.edit_map = { 'business[name]' : 'business_name' };

YUI().use("node", "io", "json",
function(Y) {
    ...
    return LocateExpress.edit_map;
});&lt;/pre&gt;&lt;a href="http://www.amazon.com/JavaScript-Good-Parts-Douglas-Crockford/dp/0596517742"&gt;&lt;img style="float:left; margin:0 10px 10px 0;cursor:pointer; cursor:hand;width: 180px; height: 236px;" src="http://3.bp.blogspot.com/-DIQ-gBUfA3E/TuoMYypXgDI/AAAAAAAAAK4/QlD7EjB3Pf8/s320/js_good_parts.gif" border="0" alt=""id="BLOGGER_PHOTO_ID_5686371099560345650" /&gt;&lt;/a&gt; &lt;p&gt;I highly recommend &lt;a href="http://www.amazon.com/JavaScript-Good-Parts-Douglas-Crockford/dp/0596517742"&gt;JavaScript: The Good Parts&lt;/a&gt; to learn about the best JavaScript has to offer and workarounds for its ugly side. The author also wrote a very popular code-checker, JSLint, which could help debug this nasty problem by highlighting implicit global variables.&lt;/p&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/7997313029981170997-7437036873655373853?l=blog.endpoint.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://blog.endpoint.com/feeds/7437036873655373853/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=7997313029981170997&amp;postID=7437036873655373853' title='1 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/7997313029981170997/posts/default/7437036873655373853'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/7997313029981170997/posts/default/7437036873655373853'/><link rel='alternate' type='text/html' href='http://blog.endpoint.com/2011/12/preventing-global-variables-in.html' title='Preventing Global Variables in JavaScript'/><author><name>Evan Tann</name><uri>https://profiles.google.com/106289380456101697641</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='//lh4.googleusercontent.com/-kSHNiNmGQFI/AAAAAAAAAAI/AAAAAAAAAEw/jhF16LMC5fw/s512-c/photo.jpg'/></author><media:thumbnail xmlns:media='http://search.yahoo.com/mrss/' url='http://4.bp.blogspot.com/-9Yv0wDxD4xg/TuoOhUECaLI/AAAAAAAAALQ/dCGHdhrCE2o/s72-c/Screen%2BShot%2B2011-12-15%2Bat%2B10.00.28%2BAM%2Bcopy.png' height='72' width='72'/><thr:total>1</thr:total></entry></feed>
