Ruby On Rails

From bibbleWiki
Jump to navigation Jump to search

Installing

Install Dependencies

sudo apt update
sudo apt install -y curl gnupg2 dirmngr git-core zlib1g-dev build-essential libssl-dev libreadline-dev libyaml-dev libsqlite3-dev sqlite3 libxml2-dev libxslt1-dev libcurl4-openssl-dev software-properties-common libffi-dev

Install Node

curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash -
sudo apt install -y nodejs

Install Yarn

curl -sL https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
sudo apt update && sudo apt install -y  yarn

Install Ruby

Takes a while so might want to add --verbose

 cd
 git clone https://github.com/rbenv/rbenv.git ~/.rbenv
 echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bashrc
 echo 'eval "$(rbenv init -)"' >> ~/.bashrc
 exec $SHELL
 
 git clone https://github.com/rbenv/ruby-build.git ~/.rbenv/plugins/ruby-build
 echo 'export PATH="$HOME/.rbenv/plugins/ruby-build/bin:$PATH"' >> ~/.bashrc
 exec $SHELL
 
 rbenv install 2.7.1
 rbenv global 2.7.1
 gem install bundler

Install Rails

 gem install rails

Creating a Project

This took a while but did finish on version 6.0.3.2 of rails

# Create 
rails new HU
# Start the server http://localhost:3000
rails server # rails s

Basics

Creating a Controller

You pass a name and an action to create a controller

rails generate controller home index

We can not change the content of the page which is found at app/views/home/index.html.erb

<h1>Welcome</h1>

Changing the default route

Lets change the default route page to be the new page. Go app/config/routes.rb
Change from

Rails.application.routes.draw do
  get 'home#index'
  # For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html
end

Change to

Rails.application.routes.draw do
  root 'home#index'
  # For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html
end

Adding a new route

Add Route

In app/config/routes.rb add a new route by adding a get with route name and a controller and action

get '/about' => 'home#about'

Add View

You will need a new view under app/views called about.html.erb

<h1>Abouty<h1>

Add Action

You will need a new action under app/controller/home_controller.rb

class HomeController < ApplicationController
...
  def about
  end
end

Layouts

Adding Bootstrap To Rails

Install Packages

yarn add bootstrap jquery popper.js

application.js

Next we go into app/javascript/packs/application.js and under the require statements

import "bootstrap/scss/bootstrap";

application.html.erb

We need to amend the layout to assume that we are using mobiles.

    <meta name="viewport" content="width=device-width, initial-scale=1">

Adding Bootstrap Components

Adding Bootstrap NavBar

We can do this by going to the bootstrap page and grab the example https://getbootstrap.com/docs/4.3/components/navbar/. The code at the time was

<nav class="navbar navbar-expand-lg navbar-light bg-light">
  <a class="navbar-brand" href="#">Navbar</a>
  <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
    <span class="navbar-toggler-icon"></span>
  </button>

  <div class="collapse navbar-collapse" id="navbarSupportedContent">
    <ul class="navbar-nav mr-auto">
      <li class="nav-item active">
        <a class="nav-link" href="#">Home <span class="sr-only">(current)</span></a>
      </li>
      <li class="nav-item">
        <a class="nav-link" href="#">Link</a>
      </li>
      <li class="nav-item dropdown">
        <a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
          Dropdown
        </a>
        <div class="dropdown-menu" aria-labelledby="navbarDropdown">
          <a class="dropdown-item" href="#">Action</a>
          <a class="dropdown-item" href="#">Another action</a>
          <div class="dropdown-divider"></div>
          <a class="dropdown-item" href="#">Something else here</a>
        </div>
      </li>
      <li class="nav-item">
        <a class="nav-link disabled" href="#" tabindex="-1" aria-disabled="true">Disabled</a>
      </li>
    </ul>
    <form class="form-inline my-2 my-lg-0">
      <input class="form-control mr-sm-2" type="search" placeholder="Search" aria-label="Search">
      <button class="btn btn-outline-success my-2 my-sm-0" type="submit">Search</button>
    </form>
  </div>
</nav>

Adding Bootstrap Modal

We can do this by going to the bootstrap page and grab the example https://getbootstrap.com/docs/4.3/components/modal/. The code at the time was

<div class="modal" tabindex="-1" role="dialog">
  <div class="modal-dialog" role="document">
    <div class="modal-content">
      <div class="modal-header">
        <h5 class="modal-title">Modal title</h5>
        <button type="button" class="close" data-dismiss="modal" aria-label="Close">
          <span aria-hidden="true">&times;</span>
        </button>
      </div>
      <div class="modal-body">
        <p>Modal body text goes here.</p>
      </div>
      <div class="modal-footer">
        <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
        <button type="button" class="btn btn-primary">Save changes</button>
      </div>
    </div>
  </div>
</div>

Adding Bootstrap Horizontal Form

We can do this by going to the bootstrap page and grab the example https://getbootstrap.com/docs/4.3/components/forms/. The code at the time was

<form>
  <div class="form-group row">
    <label for="inputEmail3" class="col-sm-2 col-form-label">Email</label>
    <div class="col-sm-10">
      <input type="email" class="form-control" id="inputEmail3" placeholder="Email">
    </div>
  </div>
  <div class="form-group row">
    <label for="inputPassword3" class="col-sm-2 col-form-label">Password</label>
    <div class="col-sm-10">
      <input type="password" class="form-control" id="inputPassword3" placeholder="Password">
    </div>
  </div>
  <fieldset class="form-group">
    <div class="row">
      <legend class="col-form-label col-sm-2 pt-0">Radios</legend>
      <div class="col-sm-10">
        <div class="form-check">
          <input class="form-check-input" type="radio" name="gridRadios" id="gridRadios1" value="option1" checked>
          <label class="form-check-label" for="gridRadios1">
            First radio
          </label>
        </div>
        <div class="form-check">
          <input class="form-check-input" type="radio" name="gridRadios" id="gridRadios2" value="option2">
          <label class="form-check-label" for="gridRadios2">
            Second radio
          </label>
        </div>
         <input class="form-check-input" type="radio" name="gridRadios" id="gridRadios3" value="option3" disabled>
         <label class="form-check-label" for="gridRadios3">
           Third disabled radio
         </label>
 </fieldset>
Checkbox
       <input class="form-check-input" type="checkbox" id="gridCheck1">
       <label class="form-check-label" for="gridCheck1">
         Example checkbox
       </label>
     <button type="submit" class="btn btn-primary">Sign in</button>

</form> </syntaxhighlight>

Adding Bootstrap Media Object (4.0)

We can do this by going to the bootstrap page and grab the example https://getbootstrap.com/docs/4.0/layout/media-object/. The code at the time was

<ul class="list-unstyled">
  <li class="media">
    <img class="mr-3" src="..." alt="Generic placeholder image">
    <div class="media-body">
      <h5 class="mt-0 mb-1">List-based media object</h5>
      Cras sit amet nibh libero, in gravida nulla. Nulla vel metus scelerisque ante sollicitudin. Cras purus odio, vestibulum in vulputate at, tempus viverra turpis. Fusce condimentum nunc ac nisi vulputate fringilla. Donec lacinia congue felis in faucibus.
    </div>
  </li>
  <li class="media my-4">
    <img class="mr-3" src="..." alt="Generic placeholder image">
    <div class="media-body">
      <h5 class="mt-0 mb-1">List-based media object</h5>
      Cras sit amet nibh libero, in gravida nulla. Nulla vel metus scelerisque ante sollicitudin. Cras purus odio, vestibulum in vulputate at, tempus viverra turpis. Fusce condimentum nunc ac nisi vulputate fringilla. Donec lacinia congue felis in faucibus.
    </div>
  </li>
  <li class="media">
    <img class="mr-3" src="..." alt="Generic placeholder image">
    <div class="media-body">
      <h5 class="mt-0 mb-1">List-based media object</h5>
      Cras sit amet nibh libero, in gravida nulla. Nulla vel metus scelerisque ante sollicitudin. Cras purus odio, vestibulum in vulputate at, tempus viverra turpis. Fusce condimentum nunc ac nisi vulputate fringilla. Donec lacinia congue felis in faucibus.
    </div>
  </li>
</ul>

Implementing the bootstrap component into Ruby On Rails

Creating the Files

  • Put the code for NavBar in a file app/views/home/_navbar.html.erb
  • Put the code for Modal in a file app/views/home/_new_question_form.html.erb
  • Put the code for Form in a file app/views/home/_new_question_form.html.erb by replacing the line "Modal body text goes here"
  • Put the code for Media Component in app/views/home/index.html.erb

Rendering Partial Views

Putting all of this code in one file is hard to maintain. We can use the render keyword in the layout to reference a form. Slightly odd but the naming convention is _<name>.html.erb for the file and <name> for the render name. So app/views/home/_navbar.html.erb is coded as below.

    <%= render 'home/navbar' %>

    <%= yield %>

    <%= render 'home/new_question_form' %>

Hooking up the Modal to Button

To hook the modal to a button you give the modal and id and you add Let's add data-toggle="modal" type="button" data-target="#new-question-modal" to the button.

In the dialog _new_question_form.html.erb

...
<div class="modal" tabindex="-1" role="dialog" id="new-question-modal">
...

In the _navbar.html.erb

Before

    <form class="form-inline my-2 my-lg-0">
      <input class="form-control mr-sm-2" type="search" placeholder="Search" aria-label="Search">
      <button class="btn btn-outline-success my-2 my-sm-0" type="submit">Search</button>
    </form>

After

    <form class="form-inline my-2 my-lg-0">
      <button class="btn btn-outline-success my-2 my-sm-0" data-toggle="modal" type="button" data-target="#new-question-modal">Ask Question</button>
    </form>

Replacing the form and image

For the form we need to replace it with this and remove the buttons from the dialog

        <form class="form-horizontal" action="/questions" method="POST">

          <div class="form-group row">
            <label for="inputEmail3" class="col-sm-2 col-form-label">Email</label>
            <div class="col-sm-10">
              <input type="email" name="email" class="form-control" id="inputEmail3" placeholder="Email" required>
            </div>
          </div>

          <div class="form-group row">
            <label for="inputQuestion" class="col-sm-2 col-form-label">Question</label>
            <div class="col-sm-10">
              <textarea name="question_body" type="password" class="form-control" id="inputPassword3" placeholder="What would you like to know" required></textarea>
            </div>
          </div>

          <div class="form-group row">
            <div class="col-sm-10">
              <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
              <button type="submit" class="btn btn-primary">Submit</button>
            </div>
          </div>
        </form>

And now let's replace the image on the media object with one from the tutorial.

Before

    <img class="mr-3" src="..." alt="Generic placeholder image">

After

Security

Similarly to the forgery tokens ruby demands we pass a token for forms. This is achieved by replacing the form tags with the for_form. The parameters are pretty obvious

<%= form_for :question, url: '/questions', html: {'class': 'form-horizontal'} do %>
...
<% end %>

The app should currently look like
Ch1 questions.png

Implementing the Answer page

Set A Get Route For Questions

Edit routes.erb to add the GET operation for questions

Rails.application.routes.draw do
  root 'home#index'

  get '/questions' => 'home#questions'

  get '/questions/:id' => 'home#questions'
  # For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html
end

Create Page for Question Get (Answers View)

Under app/views/home create questions.html.erb

<div class="container">

    <div class="lead well">
        <div class="media">
            <img class="mr-3" src="http://www.gravatar.com/avatar/424242" alt="Generic placeholder image">
            <div class="media-body">
                <h5 class="mt-0 mb-1">iwiseman@bibble.co.nz asked:</h5>
                Cras sit amet nibh libero, in gravida nulla. Nulla vel metus scelerisque ante sollicitudin. Cras purus odio, vestibulum in vulputate at, tempus viverra turpis. Fusce condimentum nunc ac nisi vulputate fringilla. Donec lacinia congue felis in faucibus.
            </div>
        </div>
    </div>

    <div class="row">
        <div class="col-sm-8 col-sm-offset-2">

        <div class="media">
            <img class="mr-3" src="http://www.gravatar.com/avatar/424242" alt="Generic placeholder image">
            <div class="media-body">
                <h5 class="mt-0 mb-1">iwiseman@bibble.co.nz answered:</h5>
                Cras sit amet nibh libero, in gravida nulla. Nulla vel metus scelerisque ante sollicitudin. Cras purus odio, vestibulum in vulputate at, tempus viverra turpis. Fusce condimentum nunc ac nisi vulputate fringilla. Donec lacinia congue felis in faucibus.
            </div>
        </div>

        <div class="media">
            <img class="mr-3" src="http://www.gravatar.com/avatar/424242" alt="Generic placeholder image">
            <div class="media-body">
                <h5 class="mt-0 mb-1">iwiseman@bibble.co.nz answered:</h5>
                Cras sit amet nibh libero, in gravida nulla. Nulla vel metus scelerisque ante sollicitudin. Cras purus odio, vestibulum in vulputate at, tempus viverra turpis. Fusce condimentum nunc ac nisi vulputate fringilla. Donec lacinia congue felis in faucibus.
            </div>
        </div>

        </div>
    </div>    

</div>

Link out Questions to the Answers

On the main page for each question add an questions link (Answer View)

<div>
    <a href="/questions/12" class="btn btn-success btn-xs">View Answers<a>
</div>

Create an Answer Modal

Copy the question modal and change the questions to answers. Add the button for Submit New Answer and the render for the modal dialog.

            <div>
                <button type="button" class="btn btn-success btn-sm" data-toggle="modal" data-target="#new-answer-modal">Submit New Answer</button>
            <div>
        
            <%= render 'home/new_answer_form' %>

Models

Creating a Model

Generating

We can create a model, including RESTFul routes, using the resource generator

# rails generate <table> <field name>:<data type> <field name>:<data type>
rails generate resource question email:string body:text

Fixing it after

The generator takes assumptions which may be wrong. In this example we need to change the contents for questions table to be not null

class CreateQuestions < ActiveRecord::Migration[6.0]
  def change
    create_table :questions do |t|
      t.string :email, null: false
      t.text :body, null: false

      t.timestamps null: false
    end
  end
end

Do Migration

To create the database table run rake

rake db:migrate

rails console

Using rails console this we can populate the database and perform sql commands

# Insert using <table>.create <field:data>,<field:data>
Question.count
Question.create email: 'iwiseman@bibble.co.nz', body: 'How is the universe?'

Using the Model

Reading the data

Read Data in Controller

You can read the data in the controller into and instance variable. This will make it available to the view.

@questions = Question.all

We can sort the data by adding order

@questions = Question.order(created_at: :desc) .all

Read Data in View

We can reading data into a view by looping over the instance variable

    <% @questions.each do | q| %>

    <% end %>

In our case

<div class="container">
        <% @questions.each do | q| %>

            <div class="media">
                <img class="mr-3" src="http://www.gravatar.com/avatar/424242" alt="Generic placeholder image">
                <div class="media-body">
                    <h5 class="mt-0 mb-1"><%= q.email %></h5>
                    <%= q.body %>
                </div>
                <div>
                    <a href="/questions/12" class="btn btn-success btn-xs">View Answers<a>
                </div>
            </div>

        <% end %>
</div>

Adding Accessor For Gravatar to Model

Add to the Model a function to get the gravatar for an email address

class Question < ApplicationRecord
  def gravatar
    "http://www.gravatar.com/avatar/#{Digest::MD5.hexdigest(email)}"
  end
end

Change the view

        <% @questions.each do | q| %>
            <div class="media">
                <img class="mr-3" src="<%= q.gravatar %>" alt="Generic placeholder image">

Improving the View

Not really Ruby-on-rails but amended the view to be improved

<div class="container">
    <% @questions.each do |q| %>

        <div class="media">

            <img class="mr-3" src="<%= q.gravatar %>">
            <div class="media-body">
                <h4 class="mt-0"><%= q.email %> asked:</h4>
                <div><%= time_ago_in_words q.created_at %> ago</div>
                <%= q.body %>
            </div>

        </div>

    <% end %>
</div>

Adding a Question (PUT)

Form Data

We need to change the form to return an array of data instead of the individual items. We can do this by changing the input modal to use an array. e.g.

          <div class="form-group row">
            <label for="inputQuestion" class="col-sm-2 col-form-label">Question</label>
            <div class="col-sm-10">
              <textarea name="question[question_body]" type="password" class="form-control" id="inputPassword3" placeholder="What would you like to know" required></textarea>
            </div>
          </div>

Create Action On Controller

Next, on the question controller we need a create method. It is important that the names all line up. IE. the names on the modal coincide with the database names. The purpose of the private function is to ensure that only the named parameters can be passed.

class QuestionsController < ApplicationController
  def create
    Question.create(question_params)
    redirect_to root_path
  end
  
  private

  def question_params
    params.require(:question).permit(:email, :body)
  end
end

Show Answer (GET)

Modify Quest List to pass Question ID

Creating a resourceful route will also expose a number of helpers to the controllers in your application. In the case of resources :photos

  • photos_path returns /photos
  • new_photo_path returns /photos/new
  • edit_photo_path(:id) returns /photos/:id/edit (for instance, edit_photo_path(10) returns /photos/10/edit)
  • photo_path(:id) returns /photos/:id (for instance, photo_path(10) returns /photos/10)


In the index page change the question item to pass the id.

<div class="container">
    <% @questions.each do |q| %>

        <div class="media">

            <img class="mr-3" src="<%= q.gravatar %>">
            <div class="media-body">
                <h4 class="mt-0"><%= q.email %> asked:</h4>
                <div><%= time_ago_in_words q.created_at %> ago</div>
                <%= q.body %>
                <div>
                    <a href="<%= question_path(q) %>" class="btn btn-success btn-xs">View Answers</a>
...

Creating Show Action

Next we need to add a show method to the question controller and retrieve the question using the parameters passed.

class QuestionsController < ApplicationController
...
    def show
        @question = Question.find(params[:id])
    end

Modify Questions Details

The original details page was called questions.erb.html. Let move this to the app/views/questions/show.html.erb and remove the hard coded question data.

<div class="container">

    <div class="lead well">

        <div class="media">

            <img class="mr-3" src="<%= @question.gravatar %>">
            <div class="media-body">
                <h4 class="mt-0"><%= @question.email %> asked:</h4>
                <div><%= time_ago_in_words @question.created_at %> ago</div>
                <%= @question.body %>
            </div>
        </div>

    </div>
...

Implementing Answer

Create the Resource

Piece of cake, use make sure you include the foreign key to question

# rails generate <table> <field name>:<data type> <field name>:<data type>
rails generate resource answer question_id:integer email:string body:text

Do Migration And Populate

Migrate and populate the database

rake db:migrate

Add some records using rails console

Answer.create question_id: 1, email: 'samer@on-site.com', body: 'Answer 1'
Answer.create question_id: 1, email: 'samer@on-site.com', body: 'Answer 2'

Modify Create Dialog

We have to modify the dialog to put the id of the question into the post. The easiest way is to add a hidden field

          <input type="hidden" name="answer[question_id]" value="<%= @question_id %>">

We need to make the parameters on the create dialog to be an array.

...
      <div class="modal-body">

        <%= form_for :answer, url: '/answers', html: {'class': 'form-horizontal'} do %>

          <input type="hidden" name="answer[question_id]" value="<%= @question.id %>">

          <div class="form-group row">
            <label for="inputEmail3" class="col-sm-2 col-form-label">Email</label>
            <div class="col-sm-10">
              <input type="email" name="answer[email]" class="form-control" id="inputEmail3" placeholder="Email" required>
            </div>
          </div>

          <div class="form-group row">
            <label for="inputAnswer" class="col-sm-2 col-form-label">Answer</label>
            <div class="col-sm-10">
              <textarea name="answer[body]" type="password" class="form-control" id="inputPassword3" placeholder="What would you like to know" required></textarea>
            </div>
          </div>
...

Relationships In Rails

Introduction

These, as you might expect, are defined on the model. With Question and Answer we have a one to many or many or one depending on which entity is on the right.

Types of Association

Rails supports six types of associations:

  • belongs_to
  • has_one
  • has_many
  • has_many :through
  • has_one :through
  • has_and_belongs_to_many

Answer/Question Example

We can define these in the models as below.

class Answer < ApplicationRecord
  belongs_to :question
end

class Question < ApplicationRecord
  has_many :answers
  def gravatar
    "http://www.gravatar.com/avatar/#{Digest::MD5.hexdigest(email)}"
  end
end

Other options are

Add Create Method

We can now get the question from the Question collection by retrieving the question id from the answer passed.
This allows us to

  • create a record on the answers collection rather than explicit question_id
  • redirect to redirect to the question.
class AnswersController < ApplicationController
  def create
    question = Question.find(params[:answer][:question_id])
    
    question.answers.create(answer_params)

    redirect_to question
  end

  def answer_params
    params.require(:answer).permit(:email, :body)
  end
end

Modify Display Answers

Because of the relationship and the instance variable question we are now able to reference the answers associated with the question

<div class="container">

    <div class="card card-body bg-light">

        <div class="media">

            <img class="mr-3" src="<%= @question.gravatar %>">
            <div class="media-body">
                <h4 class="mt-0"><%= @question.email %> asked:</h4>
                <div><%= time_ago_in_words @question.created_at %> ago</div>
                <%= @question.body %>

            <% @question.answers.each do |a| %>

                <div class="media">

                    <img class="mr-3" src="<%= a.gravatar %>">
                    <div class="media-body">
                        <h4 class="mt-0"><%= a.email %> answered:</h4>
                        <div><%= time_ago_in_words a.created_at %> ago</div>
                        <%= a.body %>
                    </div>
                </div>

            <% end %>

            <div>
                <button type="button" class="btn btn-success btn-sm" data-toggle="modal" data-target="#new-answer-modal">Submit New Answer</button>
            <div>
        </div>
            </div>

    </div>
   
    <%= render 'home/new_answer_form' %>

</div>

Enhancing the GUI

if statements in View

We can perform if statements like this in ruby on rails.

<% if @questions.empty? %>
    <div class="alert alert-info">There are not questions at this time</div>
<% end %>

Session Dictionary

We can store data in the session dictionary in ruby. This can later be retrieved by the view. For example

session[:current_user_email] = question_params[:email]

Helper Functions

Under helpers we can create functions to be share either across the application or within a view. In our case we are going to share the current_user_email function

module ApplicationHelper
    def current_user_email
        session[:current_user_email]
    end
end

Now we can change the modal to get the value from the helper function.

      <div class="modal-body">

        <%= form_for :answer, url: '/answers', html: {'class': 'form-horizontal'} do %>

          <input type="hidden" name="answer[question_id]" value="<%= @question.id %>">

          <div class="form-group row">
            <label for="inputEmail3" class="col-sm-2 col-form-label">Email</label>
            <div class="col-sm-10">
              <input type="email" name="answer[email]" value="<%= current_user_email %>" class="form-control" id="inputEmail3" placeholder="Email" required>
            </div>
          </div>

Testing in Ruby on Rails

Generate a mailer

A mailer is a model/view/controller for a mailer. We can generate this by using the rails generator and passing name of the mailer and a list of methods for each function you want.

rails generate mailer main_mailer notify_question_author

Implement Notify Question Author

This skeleton function currently looks like this

class MainMailer < ApplicationMailer
  def notify_question_author
    @greeting = "Hi"

    mail to: "to@example.org"
  end
end

We need it to receive and answer and send an email. In this we pass the answer entered, set the to: and from: emails and expose the answer as an instance variable.

  def notify_question_author(answer)
    @greeting = 'Hi'
    @answer = answer

    mail to: answer.question.email, from: answer.email
  end

Set the subject of the email

This seems a bit like voodoo but when you generate a mailer and go to the definition it provides the comment

  # Subject can be set in your I18n file at config/locales/en.yml
  # with the following lookup:
  #
  #   en.main_mailer.notify_question_author.subject
  #

So lets go to config/locales/en.yml and set it

...
  main_mailer:
    notify_question_author:
      subject: "New answer to your question"

Set the body of the email

Let's change the body of the mail to contain our new answer variable.

For Text Emails

We do this by changing the view of the mail in views/main_mailer/notify_question_author.text.erb
From

Main#notify_question_author

<%= @greeting %>, find me in app/views/main_mailer/notify_question_author.text.erb

To

<%= @greeting %>

My answer:

<%= @answer.body %>

For Html Emails

We do this by changing the view of the mail in views/main_mailer/notify_question_author.text.erb
From

Main#notify_question_author

<%= @greeting %>, find me in app/views/main_mailer/notify_question_author.text.erb

To

<%= @greeting %>

My answer:

<%= @answer.body %>

Test Cases

Initial Test Case

After creating the mailer the following test case was created

require 'test_helper'

class MainMailerTest < ActionMailer::TestCase
  test "notify_question_author" do
    mail = MainMailer.notify_question_author
    assert_equal "Notify question author", mail.subject
    assert_equal ["to@example.org"], mail.to
    assert_equal ["from@example.com"], mail.from
    assert_match "Hi", mail.body.encoded
  end
end

Test Instance

In order to test we need an instance to test with

    question = Question.create email: 'author@question.com', body: 'a test question'
    answer = Answer.create email: 'author@question.com', body: 'a test answer'

    question.answers << answer

    mail = MainMailer.notify_question_author(answer)

Fixing the asserts

Now we can fix the asserts for the test case

    assert_equal "New answer to your question", mail.subject
    assert_equal [question.email], mail.to
    assert_equal [answer.email], mail.from
    assert_match answer.body, mail.body.encoded

Using the mailer

Not hard to do. Go to the answer controller and pass the answer to the mailer.

class AnswersController < ApplicationController
  def create
    question = Question.find(params[:answer][:question_id])

    answer = question.answers.create(answer_params)

    MainMailer.notify_question_author(answer).deliver_now
...

I should mention that you can test mailers by going to the test/mailers/previews/main_mailer_preview.rb and amending the code to support it. The course person was pretty excited but I was not - at all.

  def notify_question_author
    mail = MainMailer.notify_question_author(Answer.first)
  end

So now you can go to http://localhost:3000/rails/mailers/main_mailer. Restart Server or this might fail

Active Job

This facility allows your to plug in Job adapters. Here is the web address at the time for these. We will use sucker punch. https://api.rubyonrails.org/classes/ActiveJob/QueueAdapters.html

Install Sucker Punch

This can be found at https://github.com/brandonhilkert/sucker_punch. To install add this line to gemfile

gem 'sucker_punch', '~> 2.0'

and to the application.rb

module HU2
  class Application < Rails::Application
    # Initialize configuration defaults for originally generated Rails version.
    config.load_defaults 6.0

    # Settings in config/environments/* take precedence over those specified here.
    # Application configuration can go into files in config/initializers
    # -- all .rb files in that directory are automatically loaded after loading
    # the framework and any gems in your application.

    config.active_job.queue_adapter = :sucker_punch
...

Assets

Style Sheets

Rails comes with CCS with superpowers. This allows you to add nested styles based on a div name. Style are stored under assets/stylesheets/<page>. I changed the home page to have the following.

.boxes {
    .media {
        border: thin solid #ddd;
        padding: 1em;
        border-radius: 10px;
        background-color: #ccc;
        position: relative;

        .time_format {
            position: absolute;
            top: 8px;
            right: 8px;
            color: #666
        }
        a.btn {
            position: absolute;
            bottom: 8px;
            right: 8px;
        }
        &:hover {
            cursor: pointer;
            background: #ddd
        }
    }
}

Coffee Script

This was in the course to demonstrate how to package the scripts in the app but here is what was shown. This code enables clicking of a media object instead of the need to click the button.

Install Coffee

This does not come with rails 6 so it was a bit of a hassle.

rails webpacker:install:coffee

Change Application to Scripts

In the layout application.html.erb add the include

<%= javascript_include_tag 'application', 'data-turbolinks-track' => true %>

Add Scripts

Under app/assets/javascripts/ create the following
application.js

// This is a manifest file that'll be compiled into application.js, which will include all the files
// listed below.
//
// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
// or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path.
//
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
// compiled file.
//
// Read Sprockets README (https://github.com/sstephenson/sprockets#sprockets-directives) for details
// about supported directives.
//
//= require jquery
//= require jquery_ujs
//= require turbolinks
//= require bootstrap
//= require_tree .

Note in the course there was no

'''//= require bootstrap'''

This caused the script to fail to set focus. This is because the 'shown.bs.modal is a bootstrap function.

And add the home.coffee script

ready = ->
  console.log("Testing 2")
  $(".media").on "click", ->
    document.location = $(this).data("target")
    return false

  $(".modal").on "shown.bs.modal", ->
    $(this).find("textarea").focus()

$(document).ready(ready)
$(document).on "page:load", ready

Amend View

Now amend the view (index.html.erb) use the script by adding the data-target and

<div class="container">
    <div class="boxes">
        <% @questions.each do |q| %>

            <div class="media" data-target="<%= question_path(q) %>">

                <img class="mr-3" src="<%= q.gravatar %>">
                <div class="media-body">
                    <h4 class="mt-0"><%= q.email %> asked:</h4>
                    <div class="time_format"><%= time_ago_in_words q.created_at %> ago</div>
                    <%= q.body %>
                    <div> 
                        <a href="<%= question_path(q) %>" class="btn btn-success btn-xs">View Answers</a>
                    </div>
                </div>
            </div>

        <% end %>

        <% if @questions.empty? %>
            <div class="alert alert-info">There are not questions at this time</div>
        <% end %>
    </div>

</div>

Production

Introduction

The logs for production are in

tail -200f log/production.log

Database

If you are using a database then this either needs to be migrated or if appropriate you can copy the original

cp db/development.sqlite3 db/production.sqlite3

Serving Assets

We need to tell rails we want to server our assets statically and precompile these.

export RAILS_SERVE_STATIC_FILES=true 
rm -rf node_modules
rake webpacker:clobber
yarn --check-files
rake assests:precomile
rake webpacker:compile

Minifying

You can run the server now but the code will not be minified. To minify run

RAILS_ENV=production bundle exec rake assets:precompile

Running a Production

To run the server in production mode with minifying

rails server -e production

Deployment with Heroku

Getting Started

# Install Heroku client
sudo snap install --classic heroku
# Add Project to git
heroku create
# Show all good to go
git remote -v

Move to Postgres and Add Heroku rails_12factor

rails_12factor is required to use Heroku - no idea why. We need to change the database to postgres too. First install the development libraries

sudo apt-get install libpq-dev

Now change the gemfile for the project to use pg for production and sqlite for development

 group :development do

# Use sqlite3 as the database for Active Record
gem 'sqlite3', '~> 1.4'
...
group :production do
  gem 'pg'
  gem 'rails_12factor'
end  
Now push code to heroku
<syntaxhighlight lang="bash">
rm Gemfile.lock
# As ever, we need to run bundle to update from gemfile changes
bundle install --without production

git add .
git commit -am "bundle updating sqlite3"
git push heroku master

Migrate the sqlite to postgres db

 heroku rake db:migrate

Heroku Tips

# open App
heroku open

# Logs
heroku logs --tail

# Environment
heroku config:set MYVAR=1

# Run console
heroku run rails console

# Postgres access
heroku pg:psql

# Postgres info
heroku pg:info