PizzaOnRailsApp - Rails project

Posted by Alan Krajina on September 6, 2019

In this blog I will go through project requirements and try to explain the way they are meet.

1.

Include at least one has_many, at least one belongs_to, and at least two has_many :through relationships Include a many-to-many relationship implemented with has_many :through associations. The join table must include a user-submittable attribute — that is to say, some attribute other than its foreign keys that can be submitted by the apps user

Associations are a set of macro-like class methods for tying objects together through foreign keys. Each macro adds a number of methods to the class which are specialized according to the collection or association symbol and the options hash. It works much the same way as Ruby’s own attr* methods.

Example for these associations would be Pizza model:

class Pizza < ApplicationRecord
    belongs_to :user
    belongs_to :rating
    
    has_many :meats
    has_many :cheeses
    has_many :dips
    has_many :drinks
    has_many :toppings
end

Pizza belongs_to :user and belongs_to :rating, meaning it has been given a class method of Pizza.user and Pizza.rating and through these methods we can find User and Rating attributes.

User-submittable attribute would be has_many :meats where the App user is able to choose via checkbox what type of meat he wants to add to his pizza. The class method here would be Pizza.meats with the use of iteration like:

  Pizza.meats.each do |meat| 
      meat.name
  end 

The has_many :through associations are represented by the use of two other models, User and Rating.

class User < ApplicationRecord
    has_many :pizzas
    has_many :ratings, through: :pizzas
end

class Rating < ApplicationRecord
    has_many :pizzas
    has_many :users, through: :pizzas
end

With these associations User gets the ability to call User.pizzas.rating and Rating.pizzas.user using iteration on pizzas and returning back the value of Rating or User for the specific Pizza.

The way the connection is made is by using foreign keys that are added to the pizzas table upon creation using resource generator:

rails g resource Pizza name:string delivery_address:string delivery_notes:string user_id:integer rating_id:integer

Example of User has_many :ratings, through: :pizzas would be:

class PizzasController < ApplicationController
    
  def index
...
    @pizzas = User.find(params[:user_id]).pizzas
...

Here we find a specific user by his [:user_id]) and use associations class method to access and connect to all pizzas(has_many :pizzas).

After that we save all of users pizzas to instance variable @pizzas.

The has_many :ratings, through: :pizzas is represented in Pizza index view: (part of the code)

@pizzas.each do |pizza| 
pizza.rating.stars 
pizza.rating.comment
end 

Here we iterate through the instance variable @pizzas we previously created and access stars and comment attributes from Rating. In short User has many Pizzas and many Rating attributes through Pizza.

https://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html

2.

**Your models must include reasonable validations for the simple attributes. **

You dont need to add every possible validation or duplicates, such as presence and a minimum length, but the models should defend against invalid data.

Validations are used to ensure that only valid data is saved into your database. Model-level validations are the best way to ensure that only valid data is saved into your database. They are database agnostic, cannot be bypassed by end users, and are convenient to test and maintain. Rails makes them easy to use, provides built-in helpers for common needs, and allows you to create your own validation methods as well.

Some of validations in the App:

class User < ApplicationRecord
    validates :name,  uniqueness: true
..

This validation checks for a name input that the App user is submitting upon creating a new account. Uniqueness: true evaluates is the User name input unique, meaning a User with the same name doesn’t exist in the database of previously created Users and does not allow for a User with the same name to be created again.

https://guides.rubyonrails.org/active_record_validations.html

3.

**Your forms should correctly display validation errors. **

**a. Your fields should be enclosed within a fields_with_errors class **

**b. Error messages describing the validation failures must be present within the view. **

Here we can continue on the last requirement were validations were needed.

The form with validation error would be:

<div class = "fields_with_errors"><%= render 'layouts/user_login_error' %></div>

<%= form_for @user do |f| %>
  <%= f.label :name , class: "a-primary"%>
  <%= f.text_field :name %><br />
  </br>
  <%= f.label :email , class: "a-primary"%>
  <%= f.text_field :email %><br/>
  </br>
  <%= f.label :password , class: "a-primary"%>
  <%= f.password_field :password  %><br/>
  </br>
  <%= f.submit %>
<% end %>
</div>

Here we use class = “fields_with_errors” that has custom attributes saved in application.css file:

.fields_with_errors{
    color:red;
    background-color:rgba(206, 184, 110, 0.6) ;
    width: 500px;
    padding-right: 10px;
}

The error message is shown in the users/new.html.erb with the use of partial containing the error from layouts/ _user_login_error.html.erb

<% if @user.errors.any? %>
  <div class = "fields_with_errors">
    <h2><%= pluralize(@user.errors.count, "error") %> prohibited this user from being created:</h2>
 
    <ul>
    <% @user.errors.full_messages.each do |msg| %>
      <li><em><%= msg %><em></li>
    <% end %>
    </ul>
  </div>
<% end %>

Not to forget in order for this validation failure to work we need to add the following to User model:

class User < ApplicationRecord
    has_secure_password

    validates :name,  presence: true
    validates :email, presence: true

How the errors look for the user:

4 errors prohibited this user from being created:

    Password can't be blank
    Name can't be blank
    Email can't be blank
    Email is invalid

3.

Your application must be, within reason, a DRY (Do-Not-Repeat-Yourself) rails app.

Some examples for this requirement:

class PizzasController < ApplicationController
       
  def show
    find_pizza
  end
        
  def edit
    find_pizza
  end

  def update
    find_pizza
    if @pizza.update(pizza_params)
      redirect_to pizza_path(@pizza)
    else
      render :edit
    end
  end

  def destroy
    find_pizza
    @pizza.destroy
    redirect_to user_path(current_user)
  end

  private

  def find_pizza
    @pizza = Pizza.find_by(id: params[:id])
    if !@pizza
      redirect_to pizzas_path
    end
  end

Here we see four Pizza controller actions using one method find_pizza.

And helper methods:

module ApplicationHelper
    def current_user
        @current_user ||= User.find_by_id(session[:user_id])
      end
  
      def logged_in?
        !!session[:user_id]
      end
end

https://api.rubyonrails.org/classes/ActionView/Helpers/FormHelper.html

https://guides.rubyonrails.org/layouts_and_rendering.html#using-the-content-for-method

4.

**You must include at least one class level ActiveRecord scope method. **

Your scope method must be chainable, meaning that you must use ActiveRecord Query methods within it (such as .where and .order) rather than native ruby methods (such as #find_all or #sort).

Scoping adds a class method for retrieving and querying objects. The method is intended to return an ActiveRecord::Relation object, which is composable with other scopes. If it returns nil or false, an all scope is returned instead.

Here we first add

class Pizza < ApplicationRecord
    scope :search_by_address, -> (search_address){where("delivery_address = ?", search_address)}

then

class PizzasController < ApplicationController
    
  def index
      if params[:delivery_address]
      @pizzas = Pizza.search_by_address(params[:delivery_address])

and to Pizza index.html.erb a search form

<%= form_tag("/pizzas", method: "get") do %>
<%= label_tag(:delivery_address, "Search pizza by address") %>
  <%= text_field_tag(:delivery_address) %>
  <%= submit_tag("Search") %>
<% end %>

that returns all pizzas found in database WHERE Pizza delivery address attribute matches text field input that is user creates and adds to params for delivery address.

https://api.rubyonrails.org/classes/ActiveRecord/Scoping/Named/ClassMethods.html#method-i-scope

https://guides.rubyonrails.org/active_record_querying.html

5.

You must include and make use of a nested resource with the appropriate RESTful URLs.

Config/routes.rb :

  resources :users do
    resources :pizzas, only: [:new, :create, :index]
  end

Pizzas/index.html.erb that represents INDEX nested route:

@pizzas.each do |pizza|  
link_to pizza.user.name, user_pizzas_path(pizza.user) 
end 

Users/show.html.erb that represents a NEW nested route:

<p><%= link_to '--> Create a new Pizza <--', new_user_pizza_path(@user.id), class: "a-primary3" %></p>

https://guides.rubyonrails.org/routing.html

6.

**Your application must provide standard user authentication, including signup, login, logout, and passwords. **

To pass this requirement we need to configure all aspects of the App.

Here are some signup steps:

Config/routes.rb

  get '/signup' => 'users#new'
  post '/signup' => 'users#create'

Models/users.rb and database User table needs to be created along with users_controller :

class UsersController < ApplicationController
  
      def new
          @user = User.new
      end
      
      def create
        @user = User.new(user_params)
        if @user.save
          session[:user_id] = @user.id
          redirect_to user_path(@user)
        else
          render :new
        end
      end

      private

      def user_params
        params.require(:user).permit(:name, :email, :password)
      end

Lastly we add form to create params for controller Views/users/new.html.erb:

<%= form_for @user do |f| %>
  <%= f.label :name , class: "a-primary"%>
  <%= f.text_field :name %><br />
  </br>
  <%= f.label :email , class: "a-primary"%>
  <%= f.text_field :email %><br />
  </br>
  <%= f.label :password , class: "a-primary"%>
  <%= f.password_field :password  %><br />
  </br>
  <%= f.submit %>
<% end %>
</div>

7.

**Your authentication system must also allow login from some other service. Facebook, Twitter, Foursquare, Github, etc… **

For this last requirement I won’t go into details but will just recommend this blog that phenomenally explains this step by step.

https://medium.com/@rachel.hawa/google-authentication-strategy-for-rails-5-application-cd37947d2b1b