Skip to content

Instantly share code, notes, and snippets.

@yosukehasumi
Last active February 6, 2019 01:07
Show Gist options
  • Save yosukehasumi/5e198ffac432221f83e4b586e4b77e5f to your computer and use it in GitHub Desktop.
Save yosukehasumi/5e198ffac432221f83e4b586e4b77e5f to your computer and use it in GitHub Desktop.
img240 Week 5 Assignment

Assignment 5

This week we will be adding some power and beauty to our "mint" application through database relations and a nice frontend cleanup with validations and flash messages

Setup

If you've messed up your mint application you can download and replace your entire application with the one in class repo https://gitlab.com/img240/mint just find the download button (the icon is in the right hand side and looks like a little cloud with a down arrow).

Unzip the package and it should create a directory called mint-master, copy/replace all the directory contents into your mint directory (Important! omit the .git directory though since that will have a reference to the class repo and won't allow you to push properly) Also (Important! this will replace your database.yml so if you aren't using Docker you might want to omit the config/database.yml file as well)

Make sure you can start your server and that everything is working, you might need to bundle install or bin/rails db:drop, bin/rails db:create, bin/rails db:migrate

now you can git add . and git commit -m 're-init' and git push origin master

Partials (V in MVC)

Last week we had created some layouts that used duplicate code, not very DRY... Let's fix that.

In app/views/transactions/new.html.erb and app/views/transactions/edit.html.erb we have nearly the same form except the 'destroy' and 'back to index' link. We can use the power of 'partials' to create another view that renders that form and is included in new and edit pages.

  1. Create a file called app/views/transactions/_form.html.erb and place your form code there
<%= form_for(@transaction) do |f| %>
  <div>
    <%= f.label :name %>
    <%= f.text_field :name %>
  </div>
  <div>
    <%= f.label :amount %>
    <%= f.number_field :amount %>
  </div>
  <div>
    <%= f.label :date %>
    <%= f.date_field :date %>
  </div>
  <div>
    <%= f.label :transaction_type %>
    <%= f.text_field :transaction_type %>
  </div>
  <div>
    <%= f.label :notes %>
    <%= f.text_area :notes %>
  </div>

  <div><%= f.submit %></div>
<% end %>
  1. Now we can "call" the partial from app/views/transactions/new.html.erb with:
<%= render 'form' %>
<div><%= link_to 'go back to index', transactions_path %></div>
  1. And we can "call" the partial from app/views/transactions/edit.html.erb with:
<%= render 'form' %>
<div><%= link_to 'delete', transaction_path(@transaction), method: :delete %></div>
<div><%= link_to 'go back to index', transactions_path %></div>

Make sure your forms work on the new and edit pages. Commit your changes

Validations (M in MVC)

Some of you may have noticed that our form didn't validate any of our values and allowed it to submit bad or empty values to the server. Next we will add validations on the Transaction model.

In app/models/transaction.rb let's ensure that name is present by adding validates :name, presence: true

You can test that your validation is working by starting a Rails console and typing Transaction.create, you should see an output that shows that no Transaction was inserted into the database

irb(main):001:0> Transaction.create
   (0.6ms)  SET NAMES utf8,  @@SESSION.sql_mode = CONCAT(CONCAT(@@sql_mode, ',STRICT_ALL_TABLES'), ',NO_AUTO_VALUE_ON_ZERO'),  @@SESSION.sql_auto_is_null = 0, @@SESSION.wait_timeout = 2147483
   (0.4ms)  BEGIN
   (0.5ms)  ROLLBACK
=> #<Transaction id: nil, name: nil, amount: nil, date: nil, transaction_type: nil, notes: nil>

Make it explicitly output the error message by adding a ! after .create

irb(main):002:0> Transaction.create!
   (0.6ms)  BEGIN
   (0.4ms)  ROLLBACK
Traceback (most recent call last):
        1: from (irb):2
ActiveRecord::RecordInvalid (Validation failed: Name can't be blank)

Now make it work by adding a name

irb(main):003:0> Transaction.create(name: 'Tim Hortons')
   (0.6ms)  BEGIN
  Transaction Create (1.0ms)  INSERT INTO `transactions` (`name`) VALUES ('Tim Hortons')
   (5.8ms)  COMMIT
=> #<Transaction id: 13, name: "Tim Hortons", amount: nil, date: nil, transaction_type: nil, notes: nil>

Add validations for the other columns

validates :name, presence: true
validates :amount, presence: true
validates :date, presence: true
validates :transaction_type, presence: true

We have one column that still needs validation transaction_type. For this we are going to use ActiveRecord Enums https://api.rubyonrails.org/v5.2.2/classes/ActiveRecord/Enum.html. We do this by adding enum transaction_type: { purchase: 0, deposit: 1 } to the Transaction model. This ensures that only purchase and deposit can be inserted into the database for transaction_type

All in all your model should look like this:

class Transaction < ApplicationRecord
  validates :name, presence: true
  validates :amount, presence: true
  validates :date, presence: true
  validates :transaction_type, presence: true

  enum transaction_type: { purchase: 0, deposit: 1 }
end

Final step, we need to convert the transaction_type column in the transactions table to an integer for this to work

  1. Let's make all our existing transactions "purchase" types for now: Transaction.update_all(transaction_type: 0)
  2. rails generate migration convert_transaction_type_to_int
  3. in the generated migration add change_column :transactions, :transaction_type, :integer
  4. run the migration via bin/rails db:migrate

Make sure you can start rails console and the following commands fail validation.

Transaction.create!
Transaction.create!(name: "Tim Hortons")
Transaction.create!(name: "Tim Hortons", amount: 22)
Transaction.create!(name: "Tim Hortons", amount: 22, date: Time.current)
Transaction.create!(name: "Tim Hortons", amount: 22, date: Time.current, transaction_type: :none)

and make sure this works

Transaction.create!(name: "Tim Hortons", amount: 22, date: Time.current, transaction_type: :purchase)

Commit your changes

Error and Flash Messages (V in MVC)

Now that we have validation it would be nice to have our form display those messages if a submission goes wrong. Let's add to the top of app/views/transactions/_form.html.erb a bit of code to check if we have any errors on the model.

<% if @transaction.errors.any? %>
  <% @transaction.errors.full_messages.each do |message| %>
    <div>
      <%= message %>
    </div>
  <% end %>
<% end %>

Try and use the front end form, omit the name field and you should see an error message. Try a bunch of things and see what works and what doesn't and what messages you recieve.

Commit your changes

Categories

This next section we will create another model for categories so that we can play with relationships. The following is all stuff you've done before.

  1. Generate a migration for creating the categories table bin/rails generate migration create_categories
  2. Open the migration file and add two columns, one for the name, and another for colour that we will use to make our output prettier.
create_table :categories do |t|
  t.string :name
  t.string :colour
end
  1. Run the migration bin/rails db:migrate
  2. Create the matching model app/models/category.rb
class Category < ApplicationRecord
  validates :name, presence: true
  validates :colour, presence: true
end

Check that things are working in rails console

Category.create!(name: 'Groceries', color: '#FF00FF')
Category.create!(name: 'Gas', color: '#00FFFF')
Category.create!(name: 'Rent', color: '#FFFF00')

Commit your changes

Now let's create the controller and routes

  1. In config/routes.rb add resources :categories, except: [:show]. We won't have a show view since we can display all the information on the index page
  2. Create app/controllers/categories_controller.rb and add the correct code for all actions
class CategoriesController < ApplicationController

  # GET /categories
  def index
    @categories = Category.all
  end

  # GET /categories/new
  def new
    @category = Category.new
  end

  # POST /categories
  def create
    @category = Category.new(category_params)
    if @category.save
      redirect_to categories_path
    else
      render :new
    end
  end

  # GET /categories/:id/edit
  def edit
    @category = Category.find(params[:id])
  end

  # PATCH /categories/:id
  # PUT /categories/:id
  def update
    @category = Category.find(params[:id])
    if @category.update(category_params)
      redirect_to categories_path
    else
      render :edit
    end
  end

  # DELETE /categories/:id
  def destroy
    category = Category.find(params[:id])
    category.destroy
    redirect_to categories_path
  end

  private

  def category_params
    params.require(:category).permit(:name, :colour)
  end
end

Now create some views for all three of the GET actions, plus one partial for the form

  1. app/views/categories/index.html.erb
<%= link_to 'create a new category', new_category_path %>
<% @categories.each do |category| %>
  <div>
    <%= category.name %>
    -
    <%= category.colour %>
    <%= link_to('Edit category', edit_category_path(category))%>
  </div>
<% end %>
  1. app/views/categories/edit.html.erb
<%= render 'form' %>
<div><%= link_to 'delete', category_path(@category), method: :delete %></div>
<div><%= link_to 'go back to index', categories_path %></div>
  1. app/views/categories/new.html.erb
<%= render 'form' %>
<div><%= link_to 'go back to index', categories_path %></div>
  1. app/views/categories/_form.html.erb - note that we haven't seen the select tag yet, for this we will just pass an array of colours with hex codes, maybe later we can get fancy and add a colour picker!
<%= form_for(@category) do |f| %>
  <div>
    <%= f.label :name %>
    <%= f.text_field :name %>
  </div>
  <div>
    <%= f.label :colour %>
    <%= f.select :colour, options_for_select([['red', '#00FFFF'], ['green', '#FF00FF'], ['blue', '#FFFF00']]) %>
  </div>

  <div><%= f.submit %></div>
<% end %>

Make sure that you can navigate to all the pages and submit the forms

Commit your changes

Relationships (M in MVC)

Finally let's hook up the two models so that they can interact with each other.

  1. Generate a migration to add a foreign key to the Transaction table bin/rails generate migration add_transaction_category_relation
  2. Add a column called category_id to the transactions table
add_column :transactions, :category_id, :integer
  1. Run the migration bin/rails db:migrate, this migration will add a new column to the already existing transactions table

Now that we have a new column we need to add rules for the relationship

  1. In app/models/transaction.rb add belongs_to :category
  2. In app/models/category.rb add has_many :transactions

Check that the two models are linked in rails console

irb(main):001:0> Transaction.first.category
   (0.8ms)  SET NAMES utf8,  @@SESSION.sql_mode = CONCAT(CONCAT(@@sql_mode, ',STRICT_ALL_TABLES'), ',NO_AUTO_VALUE_ON_ZERO'),  @@SESSION.sql_auto_is_null = 0, @@SESSION.wait_timeout = 2147483
  Transaction Load (0.5ms)  SELECT  `transactions`.* FROM `transactions` ORDER BY `transactions`.`id` ASC LIMIT 1
=> nil

and the other way...

irb(main):002:0> Category.first.transactions
  Category Load (0.4ms)  SELECT  `categories`.* FROM `categories` ORDER BY `categories`.`id` ASC LIMIT 1
  Transaction Load (0.7ms)  SELECT  `transactions`.* FROM `transactions` WHERE `transactions`.`category_id` = 1 LIMIT 11
=> #<ActiveRecord::Associations::CollectionProxy []>

Commit your changes

If this is working you can add the form field to app/views/transactions/_form.html.erb

<div>
  <%= f.label :category_id %>
  <%= f.select :category_id, options_from_collection_for_select(Category.all, :id, :name) %>
</div>

But remember before this will work you'll need to add category to the allowed form parameters in app/controllers/transactions_controllers.rb

def transaction_params
  params.require(:transaction).permit(:name, :amount, :date, :transaction_type, :notes, :category_id)
end

Test your form, if the form is working commit your changes

You've just created your first database relationship! Let's play with our new relationship. Go ahead and open up your /transactions index and create a new Transaction. You should be able to add a category to it now. Now in theory we can create views that show all transactions for a specific category, or show us totals for a specific date range containing "groceries and gas" that were "purchases".

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment