BRYAN TRAVIS HOOPER

July 18, 2018

My First Sinatra Project

Bryan Travis Hooper

Slowly, but surely, I am working my way through the Flatiron School's curriculum. Though it is taking me a bit longer than I expected, I also have relocated to Vermont, sold a house, and settled into my new home. So, I guess I shouldn't be too hard on myself. Living in Vermont is great and I'm hopeful that I can refocus now on getting my Flatiron work done.

To that end, I have completed my first Sinatra application. Sinatara is a framework for web apps that, along with ActiveRecord, can generate some pretty powerful CRUD based apps. Sinatra provides 'routes', which are basically pathways to content that your app will follow in response to HTTP requests. These routes allow for loading and manipulating data through Ruby methods.

For my Sinatra app, I imagined a survey system that would allow users to both create and take surveys. I made some decisions along the way to limit the scope of what I would create for this project. These limitations were selected so that I could fulfill the requirements of the Flatiron curriculum, but also get the project done without going crazy. I'm still going crazy anyway.

  • The surveys will be arbitraryily limited to five questions and each question will only have four possible answers. This is really just for the interface. The data model is designed to accomodate any number of questions and answers. I just didn't want to have to learn some JavaScript to accomplish an interface for this that made sense.
  • I did not complete the actual "taking the survey" part. Instead, in order to fulfill my requirements, I focused on the "create, edit, and delete" a survey part.

To begin, I though about my database and the relationships between the data. I came up with a model that looks something like this:

Sinatra Project Database Model

The "responses" model was not copmleted as previously indicated. So, there are four databases that I used:

  • users: This is for user information, including name, email, password, etc. I use secure passwords with bcrypt, so the database actually uses password_digest.
  • surveys: A user can have many surveys and a survey belongs to one users. Each user can create surveys, edit or delete their surveys. In theory, they could also take another persons's surveys.
  • questions: A survey can have many questions, and a question belogns to one survey.
  • answers: An answer belongs to a qeustion. But a question can have several answers. These would be presented as multiple choice options if I ever get to the "take a survey" part.

So, with that basic model in mind, I got to building the CRUD functionality, models, routes, etc.

To illustrate some of what I learned, let me go over the user authentication and login procedures:

class SessionController < ApplicationController

  get '/login' do
    if !logged_in?
      erb :'sessions/login'
    else
      @user = User.find_by(email: session[:email])
      redirect :'/'
    end
  end

  get '/registration' do
    if !logged_in?
      erb :'sessions/registration'
    else
      @user = User.find_by(email: session[:email])
      redirect :'/'
    end
  end

  post '/registration' do
     @user = User.new(username: params[:username], firstname: params[:firstname],
                      lastname: params[:lastname], password: params[:password],
                      email: params[:email])
     if !logged_in? && @user.valid?
      @user.save
      session[:email] = params[:email]
      @my_surveys = @user.surveys
      @all_surveys = Survey.all
      redirect '/surveys'
    else
      flash[:message] = ""
      @user.errors.messages.each do |key, message|
        message.each do |text|
        flash[:message] += text + "<br />"
        end
      end
      redirect :'/'
    end
  end

  post '/sessions' do
    login(params[:email], params[:password])
    @user = current_user
    @my_surveys = @user.surveys
    @all_surveys = Survey.all
    redirect '/surveys'
  end

  get '/logout' do
    session.clear
    redirect :'/'
  end

end

The SessionController manages the user login experience. But three helpers I created in ApplicationController are also relevant:


  helpers do

    def logged_in?
      !!session[:email]
    end

    def login(email, password)
      user = User.find_by(email: email)
      if user && user.authenticate(password)
       session[:email] = user.email
       session[:user_name] = user.firstname + ' ' + user.lastname
     else
       redirect '/login'
     end
    end

    def current_user
      if logged_in?
        User.find_by(email: session[:email])
      else
        redirect '/'
      end
    end

  end

These helper methods assist the login process by providing the following funtionality:

  • logged_in? simply checks the session hash for the existence of an email. Is the session has stores an email, that means the person is logged in and thus this method returns true.
  • login takes an email address and a password as inputs. It then checks to see if that user exists and the password authenticates. Password authentication is handled by bcrypt. If the user is found and authenticated, the session hash is updated. Otherwise, the user is returned to the home page.
  • current_user just returns the current users object if there is one logged in.

Now, looking back at SessionController, there are four Sinatra routes that manage the login process:

get '/login' do: This route checks to see if a user is logged in already. If not, it renders the login form. If they are logged in, the user is redirected to their home page.

get '/registration' do: This route renders the registration form if the user is not logged in already.

post '/registration' do: This route handles the registration process. This is where I used validation and flash messages to provide feedback to the user. The validation flash settings are configured in the User class:

class User < ActiveRecord::Base

  validates :username, presence: { message: "You must provide a username." },
            uniqueness: { message: "Username is already taken." }
  validates :email, presence: { message: "You must provide an email." },
            uniqueness: { message: "That email is alreday taken." }
  validates :password, presence: { message: "Password cannot be blank." }

  has_secure_password validations: false

  has_many :surveys

end

post '/sessions' do: Here, this route logs the user in and then send them to the '/surveys' route where their surveys will be listed for editing, creating, etc.

get '/logtout' do: Finally, this route clears the session hash, logging the user out.

Ok. This post is long and fairly boring, so I'm going to stop here. The CRUD aspect of this app will have to wait for another blog post. That stuff is handled by the SurveyController class. Fun!