Rails Project - Party Planner

Posted by Juny McArdle on October 29, 2019

This project was really hard for me. Rails is a very powerful framework, but in order to use this magical tool it still requires a lot of skill to utilize it. I think I just memorized the syntax and code itself rather than actually understanding the logic. As I struggled of using Rails I began to understand the framework and through this I was able to build an application that allows users to register with an email address and create parties as a host or attend parties as a guest.

Creating a new project

First, create a new project using the command below:

$ rails new rails_project_party_planner

This formulates a basic structure for you to work with. Then type bundle install in your terminal to install all the gems and dependencies that you require for the application. You will need to add a few more gems later when we work on the omniauth , bootstrap and more, but for now we will set up our models and migrations first. I used rails generator to create models and migrations. Type the following command in your terminal:

$ rails g model account email password_digest

This will create a model and a migration for Account. You can do it the same way for the other models. After creating the migrations, set the associations in the models.

class Account < ApplicationRecord
    has_secure_password
    belongs_to :accountable, :polymorphic => true, optional: true
end

class Guest < ApplicationRecord
    has_one :account, :as => :accountable
    has_many :invites
    has_many :parties, through: :invites
end

class Host < ApplicationRecord
    has_one :account, :as => :accountable
    has_many :parties
end

class Invite < ApplicationRecord
    belongs_to :party
    belongs_to :guest
end

class Party < ApplicationRecord
    belongs_to :host
    has_many :invites
    has_many :guests, through: :invites
end

Polymorphic Association

The polymophic association was complicated for me at first. I wasn’t sure why I needed it and how I was going to use it.. I was really confused. So what is a polymorphic association?

“With polymorphic associations, a model can belong to more than one other model, on a single association.” - Ruby on Rails Guide

Why did I need the polymorphic? My App allows users to sign up with two different types of accounts, as a host or a guest. So I set the Account model as the polymorphic. The Host and the Guest share the Account as :accountable. Account will have :accountable_type which is “Host” or “Guest” when it creates and :accountable_id which is same as host.id and guest.id when the Host and the Guest create. Now, they have has_one and belongs_to relationships so you will be able to call something like host.account, account.accountable_type, account.accountable. … etc.

Also, in order to make this work, you will have to set the polymophic in the migration. Add this line in the accounts table:

t.references :accountable, polymorphic: true, index: true

Run the command below to create your database:

$ rails db:migrate

Check your schema and verify everything looks good before you move on. The accounts table will look like this in the schema.

create_table "accounts", force: :cascade do |t|
    t.string "email"
    t.string "password_digest"
    t.string "accountable_type"
    t.integer "accountable_id"
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
   
    t.index ["accountable_type", "accountable_id"], name: "index_accounts_on_accountable_type_and_accountable_id"
 end

Controllers

Now, we need our controllers to implant all the logic and actions.

$ rails g controller accounts

Do the same for the other controllers. I tried to stick with RESTful routes, so my methods in the controller are pretty simple. Mainly I have all the CRUD actions. parties_controller is going to have most logic and actions. Here is what it looks like:

class PartiesController < ApplicationController
 before_action :verify_accountable
 before_action :find_party, :party_host, only: [:edit, :update, :destroy]
 
  def index
    @parties = Party.all
  end

  def new
    host?
    if params[:host_id]
      find_host
    end
    @party = Party.new
  end

  def create
    host?
    @party = Party.new(party_params)
    if params[:host_id]
      find_host
      @party.host = @host
    end
  
    if @party.save
      redirect_to host_party_path(@party.host, @party)
    else
      render :new
    end
  end

  def show
    find_party
  end

  def edit
    if authorized_host
      render :edit
    else
      flash[:danger] = "You are not an authorized user."
      redirect_to party_path(@party)
    end
  end

  def update
    if authorized_host
      @party.update(party_params)
      redirect_to host_party_path(@party.host, @party)
    else
      flash[:danger] = "You are not an authorized user."
      redirect_to parties_path
    end
  end

  def destroy
    @host = @party.host
    if authorized_host
      @party.destroy
      redirect_to host_path(current_user)
    else
      flash[:danger] = "You are not an authorized user."
      redirect_to parties_path
    end
  end

  private

  def party_params
    params.require(:party).permit(:name, :description, :location, :date_time, :dress_code, :host_id)
  end

  def find_party
    @party = Party.find(params[:id])
  end

  def find_host
    @host = Host.find(params[:host_id])
  end

  def party_host
    if params[:host_id]
      find_host
      @party.host = @host
    end
  end

  def authorized_host
    current_user == @host
  end

end

I used helpers to reduce repetition of code so it looks DRY(Don’t Repeat Yourself). You can let the helpers run before you run any actions by adding before_action on top of your controller.

Routes

After setting up all the controllers we have to set the routes. The routes will connect the controllers and views so it displays the objects and actions within the browser. Since I have the many-to-many realtionship, I used nested resources. My routes are displayed below:

# app/config/routes.rb

Rails.application.routes.draw do

  root to: 'welcome#root'
  get '/login' => 'sessions#new'
  post '/login' => 'sessions#create'
  get 'auth/:provider/callback', to: 'sessions#googleAuth'
  get 'auth/failure' => 'welcome#root'
  get '/auth_login' => 'welcome#auth_login'
  get '/logout' => 'sessions#destroy'
  delete '/logout' => 'sessions#destroy'

  
  resources :guests do
    resources :invites
  end

  resources :parties do
    resources :invites
  end

  resources :hosts do
    resources :parties
  end

  resources :accounts, only: [:new, :create]
  resources :parties
  resources :invites
  
  
end

Omniauth

Omniauth lets you utilize third-party accounts like Google, Facebook, or Twitter to authenticate and store user’s login information in your web application so users don’t have to create a separate account in your application for access. After strugging setting up the omniauth, I finaly made it to work with omniauth-google. Below is how I set this up.

First, add gem 'omniauth-google-oauth2' in your Gemfile and run bundle install to install the gem and dependencies. Create a file config/initializers/omniauth.rb and add code below:

Rails.application.config.middleware.use OmniAuth::Builder do
  provider :google_oauth2, ENV['GOOGLE_CLIENT_ID'], ENV['GOOGLE_CLIENT_SECRET']
end

You can to get the GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET from the Google Cloud Platform Console. And add this code in the Account model:

 def self.from_omniauth(auth)
      # Creates a new user only if it doesn't exist
      where(email: auth.info.email).first_or_initialize do |account|
        account.email = auth.info.email
        account.password = Sysrandom.hex(30)
      end
    end

This code will create the account object when user login. ( I installed gem 'sysrandom' for random password for users.)

def googleAuth
        # Get access tokens from the google server
        access_token = request.env["omniauth.auth"]
        @account = Account.from_omniauth(access_token)
        # log_in(@account)
        # Access_token is used to authenticate request made from the rails application to the google server
        @account.google_token = access_token.credentials.token
        # Refresh_token to request new access_token
        # Note: Refresh_token is only sent once during the first request
        refresh_token = access_token.credentials.refresh_token
        @account.google_refresh_token = refresh_token if refresh_token.present?
        if @account.save
           session[:id] = @account.id
           if @account.accountable == nil
            redirect_to auth_login_path
           elsif @account.accountable != nil
            redirect_to parties_path
           end
        else
            flash[:danger] = "Something went wrong, try again!"
            redirect_to root_path
        end

    end

I have this code above in the sessions_controller. This manages the callback from google and redirects to different paths depending on if the accountable is nil or not. The last thing you have to do is set the routes for the google callback in the config/routes.rb.

get 'auth/:provider/callback', to: 'sessions#googleAuth'
get 'auth/failure' => 'welcome#root'

Now, add the link to the “Log in with Google” in your view and you’re set!

You can run the application with code below:

$ rails s

Go to your browser and open http://localhost:3000 then you will see the root page to signup/login/logout.

Conclusion

The journey to complete this project was hard, but really rewarding to complete. It was more difficult then both the CLI and Sinatra projects. However, throughout the process I continued to learn more and get a better grasp of rails. The biggest thing that comes to mind is to understand the logic and think how to apply it in a real world scenario. If there’s something wrong then there’s reasoning behind it. I ran into several frustrating hurdles trying to complete this project that held me back, but also caused my troubleshooting skills to greatly improve in the process. Having the ability to analyze complex code and resolve errors are traits required for developers to be successful when creating a new application.