Registration form with Merb and Datamapper

This tutorial is an example of a typical registration form with Datamapper and Merb, we have:
- Unique username and email.
- Password confirm check.
- Terms and conditions agreement.
- And some more custom stuff implemented such as birthdate from year, month and day dropdowns.

screenshot.png


As I’ve no prior Ruby on Rails experience and hardly any Ruby experience the process leading up to the following result has been 2 extremely painful days. Some tips if you are in my situation:

- Chmod your gems and rdoc folders so you can read them and create symlinks to them on your desktop. They are in /var/lib/gems/1.8.

- If you have a problem check the documentation for the gem in question by browsing to it’s doc folder that you linked to above.

- If the RDocs can’t help you then browse to the source through your new symlink and check the source. Decent Ruby code is almost always easy to read, even for a newbie.

- Don’t bother much with Google when it comes to Datamapper tutorials, it’s a waste of time, your only recourse is to contact the developers on IRC or something if you still can’t manage with the RDocs, the source or the minimal stuff on datamapper.org.

- Treat documentation of similar stuff for Ruby on Rails with caution, some things are different so you don’t want to get hung up on something that works for Rails/Activerecord but not Merb/Datamapper.

Currently the best most thorough Merb + Datamapper tutorial I’ve been able to find is Kacper Ciesla’s Merb + Datamapper noob quick start. This tutorial is based on a lot of things in that tutorial, if you are totally new to Merb and Datamapper you might want to check it out. I have also documented my own experiences of setting up Merb and Datamapper.

Let’s begin at the “bottom”:

class Member < DataMapper::Base
  property :acl_id,                 :integer, :default => 1
  property :firstname,              :string
  property :lastname,               :string
  property :username,               :string
  property :password,               :string
  property :email,                  :string
  property :street,                 :string
  property :zip,                    :string
  property :city,                   :integer
  property :cellphone,              :string
  property :cellcode,               :string
  property :email_code,             :string,  :default => '0'
  property :avatar,                 :string
  property :disabled,               :boolean, :default => false
  property :has_paid,               :boolean, :default => false
  property :reminder_status,        :boolean, :default => false
  property :birthdate,              :date
  property :register_date,          :date, :default => '0001-01-01'
  property :payment_received,       :date, :default => '0001-01-01'
  property :disabled_date,          :date, :default => '0001-01-01'
  property :cancellation_date,      :date, :default => '0001-01-01'
  property :cancellation_received,  :date, :default => '0001-01-01'
  property :description,            :text
  
  attr_accessor :password_confirmation, :b_year, :b_month, :b_day, :cellprefix, :termscond
  
  validates_presence_of :street, :city, :firstname, :lastname
  #validates_acceptance_of :termscond
  #validates_confirmation_of :password
  validates_numericality_of :cellphone, :zip
  validates_uniqueness_of :username, :email
  validates_length_of :password, :username,	:minimum => 6
  validates_length_of :zip,	:minimum => 5
  validates_length_of :cellphone, :minimum => 7
  validates_format_of :email, :with => :email_address
end

class Cellprefix < DataMapper::Base
  property :prefix, :string
end

class City < DataMapper::Base
  property :name, :string
end

These are my models, noteworthy stuff:

1.) Datamapper creates unique ids per default if you omit them from your models, it’s just what I wanted, therefore they are not there.

2.) Attribute accessors are needed for non property stuff that you might want to store in your model anyway, more on that later when we look at the controller.

3.) I’ve commented out validates_acceptance_of and validates_confirmation_of because they simply wouldn’t work. They seem to be very easy to implement yet I couldn’t. I don’t know if it’s me, some bug in Datamapper’s version of validatable or simply something that has not been implemented yet (Hint: It’s probably me). After checking the gem sources I could see that Datamapper’s validatable doesn’t match the original so no recourse there (read: copy pasting). Anyway, I didn’t want to mess with the sources, that is way out of my league! So I implemented an ugly noob solution instead, more on that later.

4.) rake dm:db:automigrate works for me when I run it in the root of the project, not the other way (I can’t even remember it anymore). Beware, it currently wipes out your data!

5.) The validators can take more parameters, for instance :message => ‘your custom message here’ to override the default messages.

The controller:

class Auth < Application
  def index
    @member	= Member.new
    @ext_errors = Array.new
    render
  end

  def create
    @member = Member.new(params[:member]);
    if valid_ext?
      @member.cellphone = @member.cellprefix + @member.cellphone
      @member.birthdate = "#{@member.b_year}-#{@member.b_month}-#{@member.b_day}"
      @member.save
      render :template => 'saveok'
    else
      render :action => 'index'
    end
  end
  
  def valid_ext?
    @ext_errors = Array.new
    
    if @member.termscond == "0"
      @ext_errors << "You have to accept the terms and conditions."
    end
    
    if @member.password != @member.password_confirmation
      @ext_errors << "The passwords do not match."
    end
    
    if @ext_errors.length > 0
      return false
    end
    
    @member.valid?
  end
end

The solution to the validation problems is immediately apparent in the form of @ext_errors and valid_ext?. However, the plan is to get rid of it when I’ve managed to get to the bottom of the validates_acceptance_of and validates_confirmation_of problems. Imagine how slick and tiny the above code would be if they were working?

We start with index which displays the registration form. When the registration form is submitted create() will handle it. If our homegrown validation function val_ext? validates OK then we create a proper telephone number, birth date and then save to the database. If not then we show index again with properly prepopulated values and all, don’t ask me how it works, it just does which is wonderful.

And finally the view:

<div id="errors">
	<%= error_messages_for @member %>
	<br>
	<%= @ext_errors.join("<br>") %>
</div>
<br/>
<% form_for :member, :action => url(:auth) do %>
  <table>
	<tr>
		<td>First Name:</td>
		<td><%= text_control :firstname %></td>
	</tr>
	<tr>
		<td>Last Name:</td>
		<td><%= text_control :lastname %></td>
	</tr>
	<tr>
		<td>Username:</td>
		<td><%= text_control :username %></td>
	</tr>
	<tr>
		<td>Password:</td>
		<td><%= password_control :password %></td>
	</tr>
	<tr>
		<td>Password Again:</td>
		<td><%= password_control :password_confirmation %></td>
	</tr>
	<tr>
		<td>Email:</td>
		<td><%= text_control :email %></td>
	</tr>
	<tr>
		<td>Street:</td>
		<td><%= text_control :street %></td>
	</tr>
	<tr>
		<td>Zip:</td>
		<td><%= text_control :zip %></td>
	</tr>
	<tr>
		<td>City:</td>
		<td><%= select_control :city, :collection => City.all, :text_method => "name", :value_method => "id" %></td>
	</tr>
	<tr>
		<td>Cellphone Number:</td>
		<td>
			<%= select_control :cellprefix, :collection => Cellprefix.all, :text_method => "prefix", :value_method => "prefix" %>
			<%= text_control :cellphone %>
		</td>
	</tr>
	<tr>
		<td>Birth Date:</td>
		<td>
			<%= select_control :b_year, :collection => get_years() %>
			<%= select_control :b_month, :collection => get_months() %>
			<%= select_control :b_day, :collection => get_days() %>
		</td>
	</tr>
	<tr>
		<td colspan="2">I agree to the terms and conditions:&nbsp;<%= checkbox_control :termscond, :value => 0 %></td>
	</tr>
	<tr>
		<td colspan="2"><%= submit_button 'Save' %></td>
	</tr>
  </table>
<% end %>

This ERB file relies heavily on merb_helpers which you need to gem install and implement in the project config file. Note how we can jack straight in to the DB to get options for our select boxes, awesome! There are also some helper functions implemented in global_helper.rb which are used:

module Merb
  module GlobalHelper
    def get_years
      t_now = Time.now
      y_end = t_now.year - 18
      y_start = y_end - 70;
      (y_start..y_end).to_a
    end
	
    def get_months
      ["01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12"]
    end
    
    def get_days
      get_months + (13..31).to_a
    end
  end
end

Not much to comment on here really, note the use of 01, 02 and so on. I wasn’t sure if PostgreSQL would accept a date without the zero padding, that’s why. Oh yeah, get_years use now - 18 years which is the legal adult age in much of Europe I suppose. The services we create are usually for adults only.

And that’s that! If you are a noob like me keep that in mind and don’t treat the above code as some kind of best practice, if you’re not then feel free to comment to correct stuff that can be done in a better way.

Project source

Related Posts

Tags: , , , , ,