Skip to content

ck/cookbook

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

21 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Cookbook Application

This application we’re creating in this document serves two purposes.
First is demonstrates the use of MerbAuth slices in a Merb-based application. Second, it acts as a test harness for updates to the used gems and slices.

Overview

This document describes the steps to create a simple cookbook application using the following, best-of-bread tools:

  • Merb
  • DataMapper
  • HAML
  • RSpec
  • MerbAuth

Disclaimer: The main purpose is to show the use of MerbAuth and its slices and not the modeling of an application!

Preparation

First, let us install all necessary gems.

TODO describe the steps.

Steps

Skeleton App

Create the skeleton

$ merb-gen app cookbook --template-engine haml

$ cd cookbook

Lucky for us, the merb application generator now adds all necessary dependencies in the config/dependencies.rb:


	dependency "merb-action-args", "0.9.14"   # Provides support for querystring arguments to be passed in to controller actions
	dependency "merb-assets", "0.9.14"        # Provides link_to, asset_path, auto_link, image_tag methods (and lots more)
	dependency "merb-cache", "0.9.14"         # Provides your application with caching functions 
	dependency "merb-helpers", "0.9.14"       # Provides the form, date/time, and other helpers
	dependency "merb-mailer", "0.9.14"        # Integrates mail support via Merb Mailer
	dependency "merb-slices", "0.9.14"        # Provides a mechanism for letting plugins provide controllers, views, etc. to your app
	dependency "merb-auth", "0.9.14"          # An authentication slice (Merb's equivalent to Rails' restful authentication)
	dependency "merb-param-protection", "0.9.14"

	dependency "dm-core", "0.9.7"         # The datamapper ORM
	dependency "dm-aggregates", "0.9.7"   # Provides your DM models with count, sum, avg, min, max, etc.
	dependency "dm-migrations", "0.9.7"   # Make incremental changes to your database.
	dependency "dm-timestamps", "0.9.7"   # Automatically populate created_at, created_on, etc. when those properties are present.
	dependency "dm-types", "0.9.7"        # Provides additional types, including csv, json, yaml.
	dependency "dm-validations", "0.9.7"  # Validation framework

Note: Depending on the date and time you’re generating the application, it might have the wrong version number(s) for the datamapper dependencies.
In my case I already have 0.9.7 and needed to update it.

In order to define the connection to the database we have two options

  1. established the database connection in the environment files
  2. define the database connection parameters in a separate configuration file

We will use the old fashion method and use the config/database.yml file.
Update the file and define the connection parameters (adjust to your environment as needed):


	---
	development: &defaults
	  adapter: mysql
	  database: cookbook_development
	  username: cb_user
	  password: secret
	  host: localhost

	test:
	  <<: *defaults
	  database: cookbook_test

	production:
	  <<: *defaults
	  database: cookbook_production

	rake:
	  <<: *defaults

Refer to your database manual on create the database.

Since we use thread-safe DataMapper as our ORM, mutex can be off, which it should be.
Check the Merb.config in the config/init.rb file:


	Merb::Config.use do |c|
		c[:use_mutex] = false
	
		...
	
	end

Since we’re looking at the config/init.rb, lets change our template engine to HAML:


	...
	use_template_engine :haml
	...

That’s it for setup. We have an application that has no models, no controllers, and no views. In sort: that does nothing!

Add Authentication

Add dependencies

Next we add the user model and the proper authentication.

Since we’re very appreciative to merb for the things it does for us, we remember that
it already added the necessary dependency to merb-auth in config/dependencies.rb:


	...
	dependency "merb-auth", "0.9.14"          # An authentication slice (Merb's equivalent to Rails' restful authentication)
	...

Now if we run rake -T slices:merb-auth-slice-password we see all kinds of goodies:


	rake slices:merb-auth-slice-password:copy_assets          # Copy public assets to host application
	rake slices:merb-auth-slice-password:freeze               # Freeze MerbAuthSlicePassword into your app (only merb-auth-slice-password/app)
	rake slices:merb-auth-slice-password:freeze:app           # Freezes MerbAuthSlicePassword by copying all files from merb-auth-slice-password/ap...
	rake slices:merb-auth-slice-password:freeze:app_with_gem  # Freezes MerbAuthSlicePassword as a gem and copies over merb-auth-slice-password/app
	rake slices:merb-auth-slice-password:freeze:gem           # Freezes MerbAuthSlicePassword by installing the gem into application/gems using mer...
	rake slices:merb-auth-slice-password:freeze:models        # Freeze all models into your application for easy modification
	rake slices:merb-auth-slice-password:freeze:unpack        # Freezes MerbAuthSlicePassword by unpacking all files into your application
	rake slices:merb-auth-slice-password:freeze:views         # Freeze all views into your application for easy modification
	rake slices:merb-auth-slice-password:install              # Install MerbAuthSlicePassword
	rake slices:merb-auth-slice-password:migrate              # Migrate the database / Migrate the database
	rake slices:merb-auth-slice-password:patch                # Copy stub files and views to host application
	rake slices:merb-auth-slice-password:preflight            # Test for any dependencies / Test for any dependencies
	rake slices:merb-auth-slice-password:setup_directories    # Setup directories
	rake slices:merb-auth-slice-password:spec                 # Run slice specs within the host application context
	rake slices:merb-auth-slice-password:spec:controller      # Run all controller specs, run a spec for a specific Controller with CONTROLLER=MyCo...
	rake slices:merb-auth-slice-password:spec:default         # Run specs
	rake slices:merb-auth-slice-password:spec:html            # Run all specs and output the result in html
	rake slices:merb-auth-slice-password:spec:model           # Run all model specs, run a spec for a specific Model with MODEL=MyModel
	rake slices:merb-auth-slice-password:spec:view            # Run all view specs, run specs for a specific controller (and view) with CONTROLLER=...
	rake slices:merb-auth-slice-password:stubs                # Copy stub files to host application

We want to use it, so we run:


	$ rake slices:merb-auth-slice-password:install

Since we want to manipulate the the view, we need to freeze them with:


	$ rake slices:merb-auth-slice-password:freeze:views

Add models

Next we create the cook, our user model


	$ merb-gen model --testing-framework rspec --orm datamapper cook

and the recipe resource


	$ merb-gen resource --testing-framework rspec --orm datamapper recipe

Now we need to add the necessary properties and associations to the cook (user) model


	class Cook

	  include DataMapper::Resource

	  # Attributes
	  property :id,               Serial
	  property :login,            String
	  property :email,            String
	  property :identity_url,     String
	  property :created_at,       DateTime
	  property :updated_at,       DateTime

	  # Associations
	  has n, :recipes

	  # Validations
	  validates_present   :login, :email
	  validates_is_unique :login, :email
	  validates_format    :email, :as => :email_address

	end

Now we need to setup and configure merb-auth to do it’s magic. All configuration is done in merb/merb-auth.

First look at merb/merb-auth/setup.rb. Here we tell merb-auth to use our Cook class as the user model:


	...
	Merb::Authentication.user_class = Cook
	...

If we don’t do that, MerbAuth assumes our user class is User and creates a users table.
It also allows us to change the login and password field, e.g. to email, but for now we stick with the defaults.

Further down in the file you’ll see that it mixes the Merb::Authentication::Mixins::SaltedUser into our
user model:


	...
  # Mixin the salted user mixin
  require 'merb-auth-more/mixins/salted_user'
  Merb::Authentication.user_class.class_eval{ include Merb::Authentication::Mixins::SaltedUser }
	...

Mixing in the Merb::Authentication::Mixins::SaltedUser module does a couple of things:

  • It adds two properties:
    • crypted_password (String)
    • salt (String)
  • The new password property also gets two validations, if we require a password:
    • validation of the presence
    • validation of confirmation
  • It add the following class methods:
    • encrypt, which takes a password and salt, concatenates them with some delimiters, and encrypt the resulting string using SHA1.
    • authenticate, which takes the login and password authenticates the user. It returns the user model if authenticated, otherwise nil.
  • It add the following instance methods:
    • authenticated?, which allows system to check if user is authenticated by encrypting the passed in password and comparing it to stored, crypted password.
    • encrypt, which encrypts the password by calling the encrypt class method with the passed in password and the salt for that specific user (instance).
    • encrypt_password, which calculates the salt if it is a new user and then encrypts the password for that specific user (instance).
    • password_required?, which is used in validations.

As usual, we’re more then welcome to override the methods if we desire.

In the other file, merb/merb-auth/setup.rb, allows us to define our authentication strategies.
We’ll add support for OpenID:


	# ...
	Merb::Slices::config[:"merb-auth-slice-password"][:no_default_strategies] = true

	Merb::Authentication.activate!(:default_password_form)
	Merb::Authentication.activate!(:default_basic_auth)
	Merb::Authentication.activate!(:default_openid)

Note: There is also a lib/authentication folder with configuration files, but I believe that’s left over.

Now fill in the recipe model


	class Recipe

	  include DataMapper::Resource

	  # Attributes
	  property :id,           Serial
	  property :title,        String,  :nullable => false
	  property :instructions, Text,    :lazy => [:show]
	  property :created_at,   DateTime
	  property :updated_at,   DateTime

	  # Associations
	  belongs_to :cook

	  # Validations

	end

Now that we have our models defined, we can run our migrations


	$ rake db:automigrate

Next we need to hook up our recipe resource in the router:


	Merb::Router.prepare do
	  # RESTful routes
	  resources :recipes
		
		...
	end

At this point we can fire up merb


	$ merb
	 ~ Loaded DEVELOPMENT Environment...
	 ~ loading gem 'dm-timestamps' ...
	 ~ loading gem 'dm-types' ...
	 ~ loading gem 'dm-serializer' ...
	...

and hit the recipe list.

Next we modify the views to display the info we have and allow us to enter our recipes.
To save a bit, I’ll skip listing it. Refer to source code for details.

Configure login & logout

Now that we can enter, view and edit out recipes, we want to protect them.

To do that we need to do a couple of things.

First, lets hook up the merb_auth routes and remove the default routes:


	Merb.logger.info("Compiling routes...")
	Merb::Router.prepare do
	  # RESTful routes
	  resources :recipes

	  # Adds the required routes for merb-auth using the password slice
	  slice(:merb_auth_slice_password, :name_prefix => nil, :path_prefix => "") do
	    match("/openid").to(:controller => "sessions", :action => "update").name(:openid)
	  end
	
	  match('/').to(:controller => 'recipes', :action =>'index')
	end

Now we want to protect the recipes. We have two options.
We can either protect the controller actions by adding a before filter to the recipes controller:


	class Recipes < Application

	  before :ensure_authenticated
	
		...
	end

Or we can protect the route:


	Merb.logger.info("Compiling routes...")
	Merb::Router.prepare do
	  # RESTful routes
		authenticate do
	  	resources :recipes
		end
		
		...
		
		authenticate do
	    match('/').to(:controller => 'recipes', :action =>'index')
	  end
	end

Which solution to choose depends on the desired control we want. For now we protect the route,
which provides the benefit not even hitting any controller for unauthorized access.

As a quick and dirty test, we can create a new user via the interactive Merb shell:


	$ merb -i
	 ~ Loaded DEVELOPMENT Environment...
	 ~ loading gem 'dm-timestamps' ...
	...
	 ~ Activating slice 'MerbAuthPasswordSlice' ...
	>> joe = Cook.new(:login => 'joe', :email => 'joe@example.com', :password => 'secret', :password_confirmation => 'secret')
	=> #<Cook id=nil login="joe" email="joe@example.com" crypted_password=nil salt=nil identity_url=nil created_at=nil updated_at=nil>
	>> joe.save
	 ~ SELECT `id` FROM `cooks` WHERE (`login` = 'joe') ORDER BY `id` LIMIT 1
	 ~ SELECT `id` FROM `cooks` WHERE (`email` = 'joe@example.com') ORDER BY `id` LIMIT 1
	 ~ INSERT INTO `cooks` (`crypted_password`, `created_at`, `salt`, `updated_at`, `login`, `email`) VALUES ('65c790d0621cbc7e894320cb37622f64e621f9f0', '2008-10-02 21:06:04', '67f6635aac3645850ad5073de467a7baaab177d3', '2008-10-02 21:06:04', 'joe', 'joe@example.com')
	=> true

If we now quit the interactive Merb shell and start the Merb server, pointing the browser to http://localhost:4000/recipes should bring up the login screen.
Entering the just created login and password will then forward us to the recipes listing.

Note: I added a link in the header to logout from the application.

Protect Recipes

Right now we allow any recipe to be viewed as long as the the user can login.
Ideally we want to protect out valuable family potato soup recipe from other cooks in the system.
To do that we need to modify the recipe controller.
But first, let us add another cook to the system via the command line


	$ merb -i
	 ~ Loaded DEVELOPMENT Environment...
	 ~ loading gem 'dm-timestamps' ...
	...
	 ~ Activating slice 'MerbAuthPasswordSlice' ...
	>> bill = Cook.new(:login => 'bill', :email => 'bill@example.com', :password => 'secret', :password_confirmation => 'secret')
	=> #<Cook id=nil login="bill" email="bill@example.com" crypted_password=nil salt=nil identity_url=nil created_at=nil updated_at=nil>
	>> bill.save
	 ~ SELECT `id` FROM `cooks` WHERE (`login` = 'bill') ORDER BY `id` LIMIT 1
	 ~ SELECT `id` FROM `cooks` WHERE (`email` = 'bill@example.com') ORDER BY `id` LIMIT 1
	 ~ INSERT INTO `cooks` (`crypted_password`, `created_at`, `salt`, `updated_at`, `login`, `email`) VALUES ('e88d65490e8c3deb83845f0c49f3333f955f3fed', '2008-10-02 21:50:18', '6a2984f72020c4c6e5cd5c19a3e6760b07fe832f', '2008-10-02 21:50:18', 'bill', 'bill@example.com')
	=> true

Now lets create some recipes:


	$ merb -i
	 ~ Loaded DEVELOPMENT Environment...
	 ~ loading gem 'dm-timestamps' ...
	...
	 ~ Activating slice 'MerbAuthPasswordSlice' ...
	>> joe = Cook.first(:login => "joe")
	 ~ SELECT `id`, `login`, `email`, `crypted_password`, `salt`, `identity_url`, `created_at`, `updated_at` FROM `cooks` WHERE (`login` = 'joe') ORDER BY `id` LIMIT 1
	=> #<Cook id=1 login="joe" email="joe@example.com" crypted_password="401518266932b42163c333c973070687b731eeb4" salt="c470ddbaff0032ac297baf842463fbd7f54649f1" identity_url=nil created_at=#<DateTime: 212089759657/86400,-1/6,2299161> updated_at=#<DateTime: 212089759657/86400,-1/6,2299161>>
	>> joe.recipes.build(:title => "Potato Soup", :instructions => "Add the diced potatoes to chicken broth. Cover, and simmer for 25 minutes.").save
	 ~ SELECT `id`, `title`, `created_at`, `updated_at`, `cook_id` FROM `recipes` WHERE (`cook_id` IN (1)) ORDER BY `id`
	 ~ INSERT INTO `recipes` (`instructions`, `created_at`, `cook_id`, `title`, `updated_at`) VALUES ('Add the diced potatoes to chicken broth. Cover, and simmer for 25 minutes.', '2008-10-02 22:08:30', 1, 'Potato Soup', '2008-10-02 22:08:30')
	=> true
	>> bill = Cook.first(:login => "bill")
	 ~ SELECT `id`, `login`, `email`, `crypted_password`, `salt`, `identity_url`, `created_at`, `updated_at` FROM `cooks` WHERE (`login` = 'bill') ORDER BY `id` LIMIT 1
	=> #<Cook id=2 login="bill" email="bill@example.com" crypted_password="7143d5223fb0f71a46291bff7d4958c0ca8c7674" salt="e1455120494795a08a5214066d1da252112f2bda" identity_url=nil created_at=#<DateTime: 21208975967/8640,-1/6,2299161> updated_at=#<DateTime: 21208975967/8640,-1/6,2299161>>
	>> bill.recipes.build(:title => "Ham N Cheese Sandwich", :instructions => "Put two slices of American cheese and on slice of ham on two slices of bread.").save
	 ~ SELECT `id`, `title`, `created_at`, `updated_at`, `cook_id` FROM `recipes` WHERE (`cook_id` IN (2)) ORDER BY `id`
	 ~ INSERT INTO `recipes` (`instructions`, `created_at`, `cook_id`, `title`, `updated_at`) VALUES ('Put two slices of American cheese and on slice of ham on two slices of bread.', '2008-10-02 22:08:51', 2, 'Ham N Cheese Sandwich', '2008-10-02 22:08:51')
	=> true

Starting and logging into the application as either joe or bill shows both recipes.

First we change the index action in the recipes controller to only list recipes that belong to the cook/user in the session:


	class Recipes < Application

	  # provides :xml, :yaml, :js

		def index
		  @recipes = session.user.recipes
		  display @recipes
		end
		...
	end

But this still provides access to another cooks recipe if I enter the URL directly, e.g. for http://localhost:4000/recipes/2 .
To avoid that simply modify the show action:


	class Recipes < Application
		...
		def show(id)
		  @recipe = session.user.recipes.get(id)
		  raise NotFound unless @recipe
		  display @recipe
		end
		...
	end

We need to adjust the edit, update, and destroy actions in a similar fashion:


	class Recipes < Application
		...

	  def edit(id)
	    only_provides :html
	    @recipe = Recipe.get(id)
	    raise NotFound unless @recipe
	    display @recipe
	  end

		...
		
	  def update(recipe)
	    @recipe = session.user.recipes.get(id)
	    raise NotFound unless @recipe
	    if @recipe.update_attributes(recipe)
	       redirect resource(@recipe)
	    else
	      display @recipe, :edit
	    end
	  end

	  def destroy(id)
	    @recipe = session.user.recipes.get(id)
	    raise NotFound unless @recipe
	    if @recipe.destroy
	      redirect resource(@recipes)
	    else
	      raise InternalServerError
	    end
	  end
		...
	end

That leaves the new and create actions.
For the new action we simply add the user from the session:


	class Recipes < Application
		...
		def new
		  only_provides :html
		  @recipe = Recipe.new(:cook => session.user)
		  render
		end
		...
	end

For the create action we use the build method on the user.recipes collection:


	class Recipes < Application
		...
	  def create(recipe)
	    @recipe = session.user.recipes.build(recipe)
	    if @recipe.save
	      redirect resource(@recipe), :message => {:notice => "Recipe was successfully created"}
	    else
	      render :new
	    end
	  end
		...
	end

Unit Testing

Before we go any further, we probably should implement some unit test. Actually, we should have done it before writing any code to follow
proper Behavior Driven Development , but I guess better late then never ;).

Remember that we decided to use RSpec for unit testing when we created our project. There are other good unit testing
frameworks available for Merb and it should not be to hard to convert the tests.

Ok, lets start with the models and work out way up the stack to the views.

Unit Testing the Models

Since the Recipe model currently does not contain any testable logic, we’ll focus on the Cook model.
First, lets define a helper module to make our life easier later. It contains all attributes to create a valid Cook model


	module CookSpecHelper
	  def cook_attributes(options = {})
	    {
	      :login => 'joe',
	      :email => 'joe@example.com',
	      :password => 'secret',
	      :password_confirmation => 'secret'
	    }.merge(options)
	  end
	end

To use it, we need to include it to our first spec. At the same time, lets create a new Cook object before
each (spec) example:


	describe Cook do

	  include CookSpecHelper

	  before(:each) do
	    @cook = Cook.new
	  end

	end

With that in hand, we turn out attention to the validations in the model:


	describe Cook do
	
		...

	  it "should be invalid without a login" do
	    @cook.attributes = cook_attributes.except(:login)
	    @cook.should_not be_valid
	    @cook.errors.on(:login).should include("Login must not be blank")
	  end

	  it "should be invalid without an email" do
	    @cook.attributes = cook_attributes.except(:email)
	    @cook.should_not be_valid
	    @cook.errors.on(:email).should include("Email must not be blank")
	  end

	  it "should be valid with a full set of valid attributes" do
	    @cook.attributes = cook_attributes
	    @cook.should be_valid
	  end

	end

Notice how we use our handy cook_attributes method.
Time to run our newly created specs:


	$ rake spec
	(in /Users/ck/cookbook)
	...P

	Pending:
	Recipe should have specs (Not Yet Implemented)

	Finished in 0.026579 seconds

	4 examples, 0 failures, 1 pending

Great all of our three cook specs passed, but we have one pending spec for the recipe as a reminder.
One more thing we probably should test is the email format validation""


	describe Cook do
	
		...

		it "should be invalid without a proper email" do
		  [ "joe", "joe@", "joe example", "@example", "joe@example com" ].each do |email|
		    @cook.attributes = cook_attributes(:email => email)
		    @cook.should_not be_valid
		    @cook.errors.on(:email).should include("Email has an invalid format")
		  end
		end

	end

and re-run the specs.


	$ rake spec
	(in /Users/ck/cookbook)
	....P

	Pending:
	Recipe should have specs (Not Yet Implemented)

	Finished in 0.036376 seconds

	5 examples, 0 failures, 1 pending

Regression Tests

Now, lets turn our attention to regression testing. If you watched Yehuda Katz, aka wycats,
Testing Merb applications presentation at MerbCamp 1.0, you will feel right at home.
If you did not, stop here and take a look for the motivation of the tests.

Watched it, good! Replace the content of spec/requests/recipes_spec.rb with


	require File.join(File.dirname(__FILE__), '..', 'spec_helper.rb')

	given "the cook is authenticated" do
	  request(url(:perform_login), :method => "PUT", :params => { :login => "joe", :password => "secret" })
	end

	describe "resource(:recipes)" do

	  before(:all) do
	    DataMapper.setup(:default, "sqlite3::memory:")
	    DataMapper.auto_migrate!

	    Cook.new(:login => 'joe', :email => 'joe@example.com', :password => 'secret', :password_confirmation => 'secret').save
	  end

	  after(:each) do
	    Cook.all.destroy!
	    Recipe.all.destroy!
	  end

	  describe "GET" do

	    before(:each) do
	      @response = request(resource(:recipes))
	    end

	    it "should return 'HTTP Error 401 - Unauthorized' status" do
	      @response.status == 401
	    end

	  end

	  describe "GET (authenticatd)", :given => "the cook is authenticated" do

	    before(:each) do
	      @response = request(resource(:recipes))
	    end

	    it "should return successfully" do
	      @response.status == 200
	    end

	  end
	end

Since we’re using authenication, we added a given block, to automatically login, when needed.

Consider this the start of the regression test and feel free add more tests.

Add Activation

Until now they only way for a cook to signup and store his valuable recipes on our site, is to
contact us and request a login. That can be very time consuming and we rather spend our time
on fun stuff.

So, lets add automated signup and activation (with validation) to our application.

Add dependencies

Right now merb-auth-slice-activation is not part of merb/merb-more/merb-auth and needs
to be installed separately :


	$ git clone git://github.com/ck/merb-auth-slice-activation.git
	$ cd merb-auth-slice-activation
	$ sudo rake install

and then required explicitly


	...
	dependency "merb-auth-slice-activation", "0.9.14"
	...

Note: There is still discussion where the slice should live, as part of merb-more, as part of merb-plugins, or completely standalone. Stay tuned!

Now if we run rake -T slices:merb-auth-slice-activation we see all kinds of goodies:


	rake slices:merb-auth-slice-activation:copy_assets          # Copy public assets to host application
	rake slices:merb-auth-slice-activation:freeze               # Freeze MerbAuthSliceActivation into your app (only merb-auth-slice-activation/app)
	rake slices:merb-auth-slice-activation:freeze:app           # Freezes MerbAuthSliceActivation by copying all files from merb-auth-slice-activat...
	rake slices:merb-auth-slice-activation:freeze:app_with_gem  # Freezes MerbAuthSliceActivation as a gem and copies over merb-auth-slice-activati...
	rake slices:merb-auth-slice-activation:freeze:gem           # Freezes MerbAuthSliceActivation by installing the gem into application/gems
	rake slices:merb-auth-slice-activation:freeze:mailers       # Freeze all mailers into your application for easy modification
	rake slices:merb-auth-slice-activation:freeze:models        # Freeze all models into your application for easy modification
	rake slices:merb-auth-slice-activation:freeze:unpack        # Freezes MerbAuthSliceActivation by unpacking all files into your application
	rake slices:merb-auth-slice-activation:freeze:views         # Freeze all views into your application for easy modification
	rake slices:merb-auth-slice-activation:install              # Install MerbAuthSliceActivation
	rake slices:merb-auth-slice-activation:migrate              # Migrate the database
	rake slices:merb-auth-slice-activation:patch                # Copy stub files and views to host application
	rake slices:merb-auth-slice-activation:preflight            # Test for any dependencies
	rake slices:merb-auth-slice-activation:setup_directories    # Setup directories
	rake slices:merb-auth-slice-activation:stubs                # Copy stub files to host application

Again, to use it we run:


	$ rake slices:merb-auth-slice-activation:install

Since we want to manipulate the the view, we need to freeze them


	$ rake slices:merb-auth-slice-activation:freeze:views

and the mailers


	$ rake slices:merb-auth-slice-activation:freeze:mailers
	$ rake slices:merb-auth-slice-activation:stubs

Model Changes

Next we tell the Cook class, our user model, to support activation


	class Cook
	  include DataMapper::Resource
	  include Merb::Authentication::Mixins::ActivatedUser

	  # Attributes
		...
	end

That was easy, but what happens behind the scenes?
The Merb::Authentication::Mixins::ActivatedUser mixin does two things. First it adds
two attributes to the model:

  • :activation_code (String), for the (random) code required for a user to become active. The code will be cleared out when the user is activated.
  • :activated_at (DateTime), for the date and time the user was activated. If the value is null, the user is not active and therefore not properly authenticated.

Lets verify that by migrating our database


	$ rake db:automigrate

and checking the fields in the database.


	$ mysql -u cb_user -psecret cookbook_development
	
	mysql> describe cooks;
	+------------------+-------------+------+-----+---------+----------------+
	| Field            | Type        | Null | Key | Default | Extra          |
	+------------------+-------------+------+-----+---------+----------------+
	| activated_at     | datetime    | YES  |     | NULL    |                | 
	| activation_code  | varchar(50) | YES  |     | NULL    |                | 
	| id               | int(11)     | NO   | PRI | NULL    | auto_increment | 
	| login            | varchar(50) | YES  |     | NULL    |                | 
	| email            | varchar(50) | YES  |     | NULL    |                | 
	| identity_url     | varchar(50) | YES  |     | NULL    |                | 
	| created_at       | datetime    | YES  |     | NULL    |                | 
	| updated_at       | datetime    | YES  |     | NULL    |                | 
	| crypted_password | varchar(50) | YES  |     | NULL    |                | 
	| salt             | varchar(50) | YES  |     | NULL    |                | 
	+------------------+-------------+------+-----+---------+----------------+
	10 rows in set (0.00 sec)

Secondly, it adds the following class method

  • make_key, generates a random key using SHA1

and the following instance methods

  • activate, which activates (and saves) the user.
  • activated? / active?, returns true if the user is active, otherwise false
  • recently_activated?, returns true if the user has been activated during the current ‘request’
  • make_activation_code, creates (using class.make_key) and sets the activation code for the user

to the user model. As with the SaltedUser, we’re more then welcome to override the methods.

Configure Activation

Before activation will work, it requires some information from us.
First lets hook up the slice routes in config/router.rb:


	Merb.logger.info("Compiling routes...")
	Merb::Router.prepare do
	
		...

	  # Adds the required routes for merb-auth using the activation slice
	  slice(:merb_auth_slice_activation, :name_prefix => nil, :path_prefix => "")

	  match('/').to(:controller => 'recipes', :action =>'index')
	end

Then we need to specify the following two required slice settings:

  • :from_email, the email account to send the email from
  • :activation_host, the host to go to for activation. This is used to construct the activation link. Symbol, String or Procs are available. Procs will have the user object passed in.

We add them in the the config/init.rb:


	...
	
	Merb::BootLoader.after_app_loads do
	  # MerbAuth Slice Activation config
	  Merb::Slices::config[:merb_auth_slice_activation][:from_email]      = 'cook@cookingwithmerb.com'
	  Merb::Slices::config[:merb_auth_slice_activation][:activation_host] = 'localhost:4000'
	end

In the same block we also need to add the merb mailer configuration we want to use.
Otherwise the system won’t know by what means to delivery the emails.
We’ll use sendmail:


	...
	
	Merb::BootLoader.after_app_loads do
		Merb::Mailer.config = {:sendmail_path => '/usr/sbin/sendmail'}
		Merb::Mailer.delivery_method = :sendmail
		
	  # MerbAuth Slice Activation config
	  Merb::Slices::config[:merb_auth_slice_activation][:from_email]      = 'cook@example.com'
	  Merb::Slices::config[:merb_auth_slice_activation][:activation_host] = 'localhost:4000'
	end

Adjust your sendmail path to match your system (to determine where sendmail is located on your machine, execute which sendmail).

Note: For other mailer configuration options, like SMTP or Gmail SMTP, see MerbMailer README .

Ok, we’re done. Lets test our activation logic. Fire up interactive merb and add a user:


	~/Projects/ck/cookbook(activation) $ merb -i
	Loading init file from /Users/ck/Projects/ck/cookbook/config/init.rb
	Loading /Users/ck/Projects/ck/cookbook/config/environments/development.rb
	 ~ Connecting to database...
	 ~ Loaded slice 'MerbAuthSliceActivation' ...
	 ~ Loaded slice 'MerbAuthSlicePassword' ...
	 ~ Parent pid: 13491
	 ~ Compiling routes...
	 ~ Activating slice 'MerbAuthSliceActivation' ...
	 ~ Activating slice 'MerbAuthSlicePassword' ...
	irb(main):001:0>  joe = Cook.new(:login => 'joe', :email => 'joethecook@thisisnotmyrealemail.com', :password => 'secret', :password_confirmation => 'secret')
	=> #<Cook activated_at=nil activation_code=nil id=nil login="joe" email="joethecook@thisisnotmyrealemail.com" identity_url=nil created_at=nil updated_at=nil crypted_password=nil salt=nil>
	irb(main):002:0> joe.save
	 ~ SELECT `id` FROM `cooks` WHERE (`login` = 'joe') ORDER BY `id` LIMIT 1
	 ~ SELECT `id` FROM `cooks` WHERE (`email` = 'joethecook@thisisnotmyrealemail.com') ORDER BY `id` LIMIT 1
	 ~ INSERT INTO `cooks` (`email`, `created_at`, `salt`, `login`, `activation_code`, `crypted_password`, `updated_at`) VALUES ('joethecook@thisisnotmyrealemail.com', '2008-10-22 08:02:17', '7de6ed3df0175d8a0d22ff7b3148bbdd99d801bb', 'joe', '6345866ba898ef52f5a121f865490a26395b1c16', '37a88b4dd8e07937127f2c5f525ab62a4f590cde', '2008-10-22 08:02:17')
	 ~ Sending Signup to joethecook@thisisnotmyrealemail.com with code 6345866ba898ef52f5a121f865490a26395b1c16
	 ~ signup sent to joethecook@thisisnotmyrealemail.com about =?utf-8?Q?Please_Activate_Your_Account?=
	=> true

When we check our email, we see:


	From:	cook@cookingwithmerb.com
	Subject:	Please Activate Your Account
	Charset:	UTF-8

	Your account has been created.

	Username: joe

	Visit this url to activate your account:

	http://localhost:4000/6345866ba898ef52f5a121f865490a26395b1c16

The user is now prepped and just needs to verify his identity, i.e. email address, to become active.

Starting merb, press the link in the email, and voila Joe is active!

Customize Activation

Next we want to customize activation even more to make any new cook feel right at home and entice them to
tell their friends and colleagues about our great site.

Lets start with the email subject lines. There are two email notifications that are part of the activation process:

  1. the signup notification
  2. the welcome notification

By default they use “Please Activate Your Account” and “Welcome” as their respective email subject lines.
To change this to more site specific simply add the following lines to config/init.rb:


	...
	
	Merb::BootLoader.after_app_loads do
	
		...
	  Merb::Slices::config[:merb_auth_slice_activation][:welcome_subject]    = 'Welcome to Cookbook.com'
	  Merb::Slices::config[:merb_auth_slice_activation][:activation_subject] = 'Cookbook.com activation requested'
	end

Now, that we took care of the email subject, we turn our attention to the email bodies.
During setup we executed rake slices:merb-auth-slice-activation:stubs, which copied the two default
email templates

  1. activation.text.erb
  2. signup.text.erb

to the slice/merb-auth-slice-activation/mailers/views/activation_mailer folder.
All we need to do now is modify them to our liking.

We change signup.text.erb to


	Your account has been created.

	  Username: <%=  @user.login %>

	All that is left, is to activate your account by simply visiting:

	  <%= activation_url(@user) %>
	
	Sincerely,

	   Your Cookbook Team!

and activation.text.erb to


	Congratulation, you have been authenticated with the following email: <%= @user.email %>.
 
	You can now manage all your favorite recipes securely online without the fear of every loosing them.

	Sincerely,
	
	   Your Cookbook Team!

Appendix

Running Specs

There are multiple ways to run the specs for a Merb project. I will outline a couple of them and the order I use that I found useful.
Pick the ones you like and make sure they fit nicely in your workflow, otherwise you will abandon tests and that is dangerous!

TextMate

If you are TextMate use, you can install the RSpec Bundle (see here for instructions)
and use it to run a single example, all examples, or all examples in the folder.

One way to integrate it into your coding workflow is to

  1. Write the individual example.
  2. Run the individual example: Position cursor in example and press CMD-SHIFT-R.
  3. If the individual example passed, run all examples in the file: Position cursor in file and press CMD-R.
  4. Run all specs: Select ‘spec’ folder in folder drawer and press CMD-OPT-R.
  5. Rinse and repeat.

About

Merb demo application.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published