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
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
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.
- 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 %>
- 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>
- 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
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
- Let's make all our existing transactions "purchase" types for now:
Transaction.update_all(transaction_type: 0)
rails generate migration convert_transaction_type_to_int
- in the generated migration add
change_column :transactions, :transaction_type, :integer
- 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
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
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.
- Generate a migration for creating the
categories
tablebin/rails generate migration create_categories
- Open the migration file and add two columns, one for the
name
, and another forcolour
that we will use to make our output prettier.
create_table :categories do |t|
t.string :name
t.string :colour
end
- Run the migration
bin/rails db:migrate
- 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
- In
config/routes.rb
addresources :categories, except: [:show]
. We won't have ashow
view since we can display all the information on theindex
page - 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
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 %>
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>
app/views/categories/new.html.erb
<%= render 'form' %>
<div><%= link_to 'go back to index', categories_path %></div>
app/views/categories/_form.html.erb
- note that we haven't seen theselect
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
Finally let's hook up the two models so that they can interact with each other.
- Generate a migration to add a foreign key to the Transaction table
bin/rails generate migration add_transaction_category_relation
- Add a column called
category_id
to thetransactions
table
add_column :transactions, :category_id, :integer
- Run the migration
bin/rails db:migrate
, this migration will add a new column to the already existingtransactions
table
Now that we have a new column we need to add rules for the relationship
- In
app/models/transaction.rb
addbelongs_to :category
- In
app/models/category.rb
addhas_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".