Fruit Trees Sinatra App

Posted by Kate Jewett Williams on February 1, 2019

Wow. It’s taken me longer to get here than I would have imagined but I’m really pleased with this app. It may not be all that powerful or pretty but it does the job. I think in the future I might return to it and add some styling and functionality. Right now I need to just be happy with a project that delivers a minimum viable product and move on to bigger things in this course.

The app is a MVC-CRUD applications with two models, Homeowners and Trees. Homeowners have many Trees and a Tree belongs to a Homeowner. Homeowners must signup with a first and last name, email address and passowrd. Trees must be created with a variety, size and fruit weight. Fruit weight is the weight of the harvest fruit in pounds.

I made a bit of a stretch goal for myself to use a little bit of the Active Record(AR) validations avialable instead of creating my own. I only did this for one model so I could demonstrate that I understood how to do it both with AR and in without. My Homeowner model looks like this:

class Homeowner < ActiveRecord::Base
	has_secure_password
	validates :first_name, presence: true
	validates :last_name, presence: true
	validates :email, presence: true
	validates :email, uniqueness: true
	
	has_many :trees
end

With these AR validations I can mass-assign my Homeowner attributes instead of having to check if each attribute is filled in the form.

post '/homeowners' do 
    **@homeowner = Homeowner.new(params)**
    if @homeowner.save
      session[:homeowner_id] = @homeowner.id
			...

vs

post '/trees' do
	  redirect_if_not_logged_in
	  if form_is_filled?
	    flash[:message] = "Your tree was successfully created"
	    **@tree = Tree.create(variety: params[:variety], size: params[:size], fruit_weight: params[:fruit_weight], homeowner_id: current_user.id)**
	    redirect "/trees/#{@tree.id}"
	...
	
	def form_is_filled?
		params[:variety] != "" && params[:size] != "" && params[:fruit_weight] != ""
	end

The pattern is a little different but with the AR validations requiring the presence for all my attributes I only need to check if I can save the object after instaniating a new one rather than having to check that the form is filled and then creating(which saves the object) an object with each param set manually. If you have a realy long form that could get very messy.

AR validations also come with specific error messages that you can use to improve user experience. If I try to create a new Homeowner without the required attributes I can see in tux that the database will rollback and not allow that Homeowner to be persistied.

>> homeowner = Homeowner.create
D, [2019-02-01T11:23:51.189457 #3820] DEBUG -- :    (0.1ms)  begin transaction
D, [2019-02-01T11:23:51.246680 #3820] DEBUG -- :   Homeowner Exists (0.2ms)  SELECT  1 AS one FROM "homeowners" WHERE "homeowners"."email" IS NULL LIMIT 1
D, [2019-02-01T11:23:51.247128 #3820] DEBUG -- :    (0.1ms)  rollback transaction
=> #<Homeowner id: nil, first_name: nil, last_name: nil, email: nil, password_digest: nil, created_at: nil, updated_at: nil>

In catching that rollback transaction, I can then check the errors on the instance I tried to create.

>> homeowner.errors
=> #<ActiveModel::Errors:0x007fc7bcd02588 
@base=#<Homeowner id: nil, first_name: nil, last_name: nil,
email: nil, password_digest: nil, created_at: nil, updated_at: nil>,
@messages={:password=>["can't be blank"],
:first_name=>["can't be blank"], 
:last_name=>["can't be blank"], 
:email=>["can't be blank"]}>

To expose just the messages and not the object I use the full_messages method

>> homeowner.errors.full_messages
=> ["Password can't be blank", 
"First name can't be blank", 
"Last name can't be blank", 
Email can't be blank"]

and then to use those with Flash I need to extract them from the array with the to_sentence method which gives me a string.

>> homeowner.errors.full_messages.to_sentence
=> "Password can't be blank, 
First name can't be blank, 
Last name can't be blank, 
and Email can't be blank"

I’m also validating the uniqueness of the email attribute so that a homeowner can’t sign up twice with the same email. In this example I’ve already used the email kate@me.org for a homeowner.

>> homeowner.first_name = "bob"
=> "bob"
>> homeowner.last_name = "jones"
=> "jones"
>> homeowner.email = "kate@me.org"
=> "kate@me.org"
>> homeowner.password = "test"
=> "test"
>> homeowner.save
D, [2019-02-01T11:34:59.902993 #3820] DEBUG -- :    (0.2ms)  begin transaction
D, [2019-02-01T11:34:59.905018 #3820] DEBUG -- :   Homeowner Exists (0.3ms)  SELECT  1 AS one FROM "homeowners" WHERE "homeowners"."email" = 'kate@me.org' LIMIT 1
D, [2019-02-01T11:34:59.907237 #3820] DEBUG -- :    (0.1ms)  rollback transaction
=> false
>> homeowner.errors.full_messages.to_sentence
=> "Email has already been taken"

I can then use these specific messages to create flash messages for useres using interpolation.

 flash[:errors] = "Account creation failure: #{@homeowner.errors.full_messages.to_sentence}"
      redirect '/signup'

There is a lot more to Active Record validations but I am pretty excited to see what they can with future projects. If you want to check out more about them I would start here!