News

Welcome to End Point’s blog

Ongoing observations by End Point people

Ruby Ecommerce with Sinatra: Admin and Products

Last week, I wrote about creating a very simple ecommerce application on Ruby with Sinatra. This week, we continue on the yellow brick road of ecommerce development on Ruby with Sinatra.

yellow brick road
A yellow brick road.

Part 2: Basic Admin Authentication

After you've got a basic application running which accepts payment for a single product as described in the previous tutorial, the next step is to add admin authorization to allow lookup of completed orders. I found several great resources for this as well as a few Sinatra extensions that may be useful. For the first increment of implementation, I followed the instructions here, which uses Basic::Auth. The resulting code can be viewed here. I also introduce subclassing of Sinatra::Base, which allows us to keep our files a bit more modular and organized.

And if we add an "/admin" method to display orders, we can see our completed orders:


Completed orders.

Part 3: Introducing Products

Now, let's imagine an ecommerce store with different products! Whoa! For this increment, let's limit each order to one product. A migration and model definition is created to introduce products, which contains a name, description, and price. For this increment, product images match the product name and live ~/public/images. The orders table is modified to contain a reference to products.id. The orders model is updated to belong_to :products. Finally, the frontend authorization method is modified to use the order.product.price in the transaction.

# Products ActiveRecord migration
require 'lib/model/product'
class CreateProducts < ActiveRecord::Migration
  def self.up
    create_table :products do |t|
      t.string :name,
        :null => false
      t.decimal :price,
        :null => false
      t.string :description,
        :null => false
    end
  end

  def self.down
    drop_table :products
  end
end
 
# Products model class
class Product < ActiveRecord::Base
  validates_presence_of :name
  validates_presence_of :price
  validates_numericality_of :price
  validates_presence_of :description

  has_many :orders
end
# Order migration update
class CreateOrders < ActiveRecord::Migration
  def self.up
    create_table :orders do |t|
+      t.references :product,
+        :null => false
    end
  end

  def self.down
    drop_table :orders
  end
end
# Order model changes
class Order < ActiveRecord::Base
...
+ validates_presence_of :product_id
+
+  belongs_to :product
end
# in main checkout action
# Authorization amount update
- response = gateway.authorize(1000,
-   credit_card)
+ response = gateway.authorize(order.product.price*100,
+   credit_card)


Our new data model.

And let's use Sinatra's simple and powerful routing to build resource management functionality that allows our admin to list, create, update, and delete items, or in this case orders and products. Here's the sinatra code that accomplishes this basic resource management:

# List items
app.get '/admin/:type' do |type|
  require_administrative_privileges
  content_type :json

  begin
    klass = type.camelize.constantize
    objects = klass.all
    status 200
    objects.to_json
  rescue Exception => e
    halt 500, [e.message].to_json 
  end
end
# Delete item
app.delete '/admin/:type/:id' do |type, id|
  require_administrative_privileges
  content_type :json

  begin
    klass = type.camelize.constantize
    instance = klass.find(id)
    if instance.destroy
      status 200
    else
      status 400
      errors = instance.errors.full_messages
      [errors.first].to_json
    end
  rescue Exception => e
    halt 500, [e.message].to_json
  end
end
# Create new item
app.post '/admin/:type/new' do |type|
  require_administrative_privileges
  content_type :json
  input = json_to_hash(request.body.read.to_s)
 
  begin
    klass = type.camelize.constantize
    instance = klass.new(input)
    if instance.save
      status 200
      instance.to_json
    else
      status 400
      errors = instance.errors.full_messages
      [errors.first].to_json
    end
  rescue Exception => e
    halt 500, [e.message].to_json
  end
end
# Edit item
app.post '/admin/:type/:id' do |type, id|
  require_administrative_privileges
  content_type :json
  input = json_to_hash(request.body.read.to_s)
  
  begin
    klass = type.camelize.constantize
    instance = klass.find(id)
    if instance.update_attributes(input)
      status 200
      instance.to_json
    else
      status 400
      errors = instance.errors.full_messages
      [errors.first].to_json
    end
  rescue Exception => e
    halt 500, [e.message].to_json
  end
end

Note that in the code shown above, the request includes the class (product or order in this application), and the id of the item in some cases. The constantize method is used to get the class constant, and ActiveRecord methods are used to retrieve and edit, create, or delete the instance. This powerful routing now allows us to easily manage additional resources with minimal changes to our server-side code.

Next, I use jQuery to call these methods via AJAX, also in such a way that it'll be easy to manage new resources with minimal client side code. That base admin code can be found here. With this jQuery admin base, we now define our empty resource, content for displaying that resource, and content for editing that resource. Examples of this are shown below:

functions.product = {
  edit: function(product) {
    return '<h4>Editing Product: '
      + product.id
      + '</h4>'
      + '<p><label for="name">Name</label>'
      + '<input type="text" name="name" value="'
      + product.name
      + '" /></p>'
      + '<p><label for="price">Price</label>'
      + '<input type="text" name="price" value="'
      + parseFloat(product.price).toFixed(2)
      + '" /></p>'
      + '<p><label for="description">Description</label>'
      + '<textarea name="description">'
      + product.description
      + '</textarea></p>';
  },
  content: function(product) {
    var inner_html = '<h4>Product: '
      + product.id
      + '</h4>'
      + 'Name: '
      + product.name
      + '<br />Price: $'
      + parseFloat(product.price).toFixed(2)
      + '<br />Description: '
      + product.description
      + '<br />';
    return inner_html;
  },
  empty: function() {
    return { name: '',
      price: 0, 
      description: '' };  
  }
};


Product listing.

Creating a new product.

Editing an existing product.

functions.order = {
  edit: function(order) {
    return '<b>Order: '
      + order.id
      + '</b><br />'
      + '<input name="email" value="'
      + order.email
      + '" />'
      + ' – '
      ...
      //Order editing is limited
  },
  content: function(order) {
    return '<b>Order: '
      + order.id
      + '</b><br />'
      + order.email
      + ' – '
      + order.phone
      + '<br />'
      ...
  },
  empty: function() {
    return { 
      email: '',
      phone: '',
      ...
    };  
  }
};


For this example, we limit order editing to email and phone number changes.

With a final touch of frontend JavaScript and CSS changes, the following screenshots show the two customer-facing pages from our example store. Like the application described in the previous article, this ecommerce application is still fairly lightweight, but it now allows us to sell several products and manage our resources via the admin panel. Stay tuned for the next increment!

The cupcake images shown in this article are under the Creative Commons license and can be found here, here, and here. The code shown in this article can be found here (branches part2 and part3).

4 comments:

Google Book said...

I went to the link but only found the code for your 1st sinatra piece. Not this new one.

Am I missing something ?

Thanks,
John

Steph Skardal said...

The code is organized into several branches: the master branch only has the code tied to the first article I wrote, part2 & part3 branches have code tied to this article, and the "collective" branch has a lot more features that are tied to future articles.

Google Book said...

Very cool.

Looks great. I can get the 1st sample to run, but when I click submit it gives my an sql error.

I can see that the database is there but it doesnt seem to want to create/add the order.

John

Google Book said...

This is a nice set of articles.

And I now have the 1st working. But i am having trouble with the 2nd.

I keep getting:

store.rb:37:in `Store': uninitialized constant Sinatra::Configuration (NameError
)
from store.rb:7:in `(root)'

Makes no sense, i can see in the store.rb where it creates the Store class. I am using JRuby, would that be a problem ?

Thanks,
JT