Sinatra project mode Pokemon

Posted by Cody Frank on August 22, 2019

At last the long awaited Sinatra project has arrived

Over the last 7 or so weeks I have taken a dive into the world of Sinatra web development. This feels very rewarding as I have spent the prior 8 months dedicating my life to writing code and still have no idea how to get out of the terminal. Finally no more “puts”ing everything out.

The Objective

  • Build an MVC Sinatra application.
  • Use ActiveRecord with Sinatra.
  • Use multiple models.
  • Use at least one has_many relationship on a User model and one belongs_to relationship on another model.
  • Must have user accounts - users must be able to sign up, sign in, and sign out.
  • Validate uniqueness of user login attribute (username or email).
  • Once logged in, a user must have the ability to create, read, update and destroy the resource that belongs_to user.
  • Ensure that users can edit and delete only their own resources - not resources created by other users.
  • Validate user input so bad data cannot be persisted to the database.
  • BONUS: Display validation failures to user with error messages. (This is an optional feature, challenge yourself and give it a shot!)

Build an MVC Sinatra application.

This requirement felt very trivial and obvious as I spent the last 7 weeks in Flatiron learning how to use Sinatra and the MVC (Model View Controller) architecture style comes very easy to me. The MVC points to a way of structuring a file tree (and what is in those files) into 3 main catagories.

Models

This is where your classes are defined and where the logic in your application lives. My project has two models, Trainers and Pokemon. The model files include class definitions, has_many/belongs too relationship, and user validations.

class Trainer < ActiveRecord::Base

    validates_presence_of :email, :trainer_name
    validates_uniqueness_of :email, :trainer_name
    has_secure_password
    validates :password, length: { minimum: 5 }, confirmation: true, 
      unless: Proc.new { |t| t.password.blank? }
    has_many :pokemon
end

These classes inherite from ActiveRecord::Base(see active record section) and that gives them access to the metaprograming seen in the class

Views

This is where the HTML(hypertext markup language) and CSS (cascading style sheets) live. These two file types work together with ERB (embedded Ruby) to display your content to the user. The HTML is responsible for the content. The CSS is responsible for making it look good. Finally the ERB is just Ruby code that lives in this file to give some interaction to what the user sees. I attempted to keep as much Ruby code out of the views as possible as it is not the best location for it howevery the occasional if statement is needed.

<%if @failed%>

<% @trainer.errors.full_messages.each do |e|%>

<p class="failed"><%= e %></p>

<%end%>

<%end%>

The code above was used in my project to show error messages in my signup page (see bonus section for more details). It is a combination of HTML and Ruby inside of and ERB file. my views are further broken down into three sections Trainer, Pokemon, and Sessions. These directories hold the files that relate to those models and sessions for loging in/out and signing up.

Controllers

Controllers are used much like they were in the cli project by bridging the gap from the views and models. The controller is responsible for routes

get '/' do

erb :index

end

These routes direct the application based on what the user puts into the url. the root route ‘/’ would display the index.erb view to the user when the route ‘/’ is entered into the url. a user changes what the application shows them by manipulating the url.

use ActiveRecord and Sinatra

This also seemed like a no brainer to me as described above ActiveRecord was used in the models to give access to many methods. The function of these methods (and ActiveRecord) revolve around manipulating a database. In this case a SQLite3 Database. The ActiveRecord gem takes the task of writing SQLite3 and turns it into Ruby that Flatiron has been teaching us all about. Sinatra is the gem that is responsible for making the routes possible. Without Sinatra the application wouldn’t have any url manipulation and without that a user would not be able to see any of the content or use the application at all.

    configure do
        set :views, 'app/views'
        set :public_folder, 'public'
        enable :sessions
        set :session_secret, SESSION_SECRET
    end
@pokemon = Pokemon.find_by_id(params[:id])
@pokemon.trainer.id

Use multiple models and Use at least one has_many relationship on a User model and one belongs_to relationship on another model

This was shown in the MCV section as well where I used a Trainer model and Pokemon model. Trainers are like users where they have many pokemon and the Trainer info is used for signing in and out. Pokemon are the belongs too part of the relationship where a Trainer collects Pokemon.

Must have user accounts - users must be able to sign up, sign in, and sign out.

This is where the Sessions Comes in

        enable :sessions
        set :session_secret, SESSION_SECRET

Sessions are Store as cookies that hold info about the user in the users browser. These cookies get sent back and forth on every request and continually validate the user allowing them to be logged in. This is done by turning sessions on and manipulating the sessions hash (seen below) to allow the application to persist state (or log in and out)

session[:user_id] = @trainer.id

## Validate uniqueness of user login attribute (username or email). This was mostly completed with metaprogramming validates_uniqueness_of :email, :trainer_name this seems like sudo code but it is actually ActiveRecord building a valid? method that I use explicitly @trainer.valid? in if statements to build errors and implicitly in the .save method @trainer.save the save method calls valid? method and will only save if valid? returns true.

## Once logged in, a user must have the ability to create, read, update and destroy the resource that belongs_to user. The resoure that belongs_to the user is the Pokemon. In my project I used RESTful routes to to perform the CRUD actions.

  • Create
    pokemon = Pokemon.create(name: params[:name], nickname: params[:nickname], element: params[:element])
    if pokemon.valid?
        current_user.pokemon << pokemon
        redirect "/trainers/#{current_user.id}"
    else
        @messages = "There was a problem catching your pokemon. Please make sure all of the boxes are filled in"
        erb :'pokemon/new'
    end
  • Read
    if @pokemon = Pokemon.find_by_id(params[:id])
      erb :'pokemon/show'
    else
      @messages = "oops! looks like that pokemon doesn't exist please try again."
      erb :'error'
    end
  • Update
 if @pokemon = Pokemon.find_by_id(params[:id])
      if @pokemon.trainer.id == current_user.id
  
          if params[:name] != ""
            @pokemon.name = params[:name]
          end
  
          if params[:nickname] != ""
            @pokemon.nickname = params[:nickname]
          end
  
          if params[:element] != ""
            @pokemon.element = params[:element]
          end
  
          if @pokemon.valid?
            @pokemon.save
            redirect "pokemon/#{@pokemon.id}"
          else
            @messages = "Something has gone wrong"
            erb :'pokemon/edit'
          end
      else
          @messages = "That is not your Pokemon to edit"
          erb :error
      end
    else 
      @messages = "oops! looks like that pokemon doesn't exist please try again."
      erb :error
    end
  • Destroy
    if @pokemon = Pokemon.find_by_id(params[:id])
      if @pokemon.trainer.id == current_user.id
          @pokemon.delete
          redirect '/pokemon'
      else 
          redirect "/pokemon/#{@pokemon.id}"
      end
    else
      @messages - "oops! looks like that pokemon doesn't exist please try again."
      erb :error
    end
  end

This is the main User interaction with Pokemon and what my project was based around. all of these routes start with the authenticate helper method. This method uses another helper method logged_in? to only allow a logged in user to perform CRUD.

      def logged_in?
        !!session[:user_id]
      end
      def authenticate
        if !logged_in?
          redirect '/login'
        end
      end

Ensure that users can edit and delete only their own resources - not resources created by other users.

This requirement is mostly completed with the current_user helper

      def current_user
        Trainer.find_by_id(session[:user_id])
      end

verifiying the the Pokemon’s trainer is the same as the current user like so

    if @pokemon = Pokemon.find_by_id(params[:id])
      if @pokemon.trainer.id == current_user.id

this way if the current user does not own that object the user cannot manipulate it.

Validate user input so bad data cannot be persisted to the database.

This requirement I found very difficult. In the end here is what I came up with.

      def clean(params)
        new_params = params.dup
        params.each do |k, v|
          new_params[k] = Rack::Utils.escape_html(v)
        end
        return new_params
      end

A helper method called clean that uses Rack::Utils method escape_html to sanitize the data a user puts into the program. This prevents the user from entering bad data and messing with my project. It works by taking an argument of a hash. We then pass the params hash (the users input data) into our helper method. This creates a copy of the hash that has been sanitized that gets used on our program. The routes look like this.

  clean_params = clean(params)
    pokemon = Pokemon.create(name:clean_params[:name], nickname: clean_params[:nickname], element: clean_params[:element])

BONUS: Display validation failures to user with error messages.

This is were the ERB really came into play. Inside my routes I set an instance variable @failed equal to false. if the validation failed the @failed would then be changed to true. It looks like this.

      post '/login' do
        @trainer = Trainer.find_by_trainer_name(params[:trainer_name])
        if !!@trainer && @trainer.authenticate(params[:password])
          session[:user_id] = @trainer.id
          redirect "/trainers"
        else
          @failed = true
          erb :'sessions/login'
        end

with the @failed declaired in the get request route. Then in the ERB view file I put an if statement saying if @failed is true then show error message. That looks like this.

<%if @failed%>
<p class="failed">Incorrect Trainer Name or Password</p>
<%end%>

The message in this case is always the same. That gives a potential Hacker the least amount of info as to make it more difficult for them. However the signup method has route set up the same way but uses the error messages built into the vaild? method i mentioned earlier. This allows the error messages to be more descriptive when signing up for easier user interaction. it looks like this.

<%if @failed%>
  <% @trainer.errors.full_messages.each do |e|%>
    <p class="failed"><%= e %></p>
  <%end%>
<%end%>

conclusion

I have now completed my Sinatra Project and feel as I have learned far more in two weeks of a project then the past two months of coding. I am very thankful for the strict requirments on this project for without them I would have never pressed myself to get to the next level.