This week we'll be creating a "Personal Finance" web app, where you can keep track of your budget and spending
The app will just have one controller that will list your spending and budget. It will allow you to add, modify and delete spending from the database.
- create a repo project in gitlab called 'mint', this should be in your "student" repo!
- in command line go to your workspace root directory and create the Rails application
rails new mint --database=mysql
some of you may have noticed that running rails new
will automatically initialize a git repository, you can see this from the .git
directory in the root of your Rails project
- in command line you'll need to change directory into your project
cd mint
- in gitlab get your repo url, look for the
clone
dropdown in your project dashboard - in command line you need to add the address of your gitlab repo to your git repository, do:
git remote add origin YOUR_REPO_URL
, this adds your repo url to theorigin
namespace - you'll need to stage all the files you just created when you did the
rails new
command. In command line dogit add .
- save your changes to a commit
git commit -m 'init'
, get into the habit of making lots of commits.
some of you might get a message *** Please tell me who you are.
if so you can run git config --global user.email "[email protected]"
, this ensures git can attribute your commit to an identity
- now push your changes,
git push origin master
Right now you still probably won't be able to start your application but you can try rails server
and check that http://localhost:3000/
works.
I can see that my default database credentials in database.yml
are incorrect for my docker setup
default: &default
adapter: mysql2
encoding: utf8
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
username: root
password:
host: localhost
We need to change the host to mysql
and the password
since that's how I've mapped the docker configuration
default: &default
adapter: mysql2
encoding: utf8
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
username: root
password: root
host: mysql
We still aren't quite there yet if you try rails server
and check http://localhost:3000/
you'll probably see Unknown database 'mint_development'
Let's tell Rails to create a database for us bin/rake db:create
root@ac48fdbbbfaa:/workspace/mint# bin/rake db:create
Running via Spring preloader in process 368
Created database 'mint_development'
Created database 'mint_test'
Do you see Yay! You’re on Rails!
? This is good news. We have a working app! Best to save our progress by creating a commit.
git add .
git commit -m 'Rails is working!'
git push origin master
Now that we have a terminal running our server it would sure be nice to have another terminal to have another command line wouldn't it? you can do docker exec -it img240 /bin/bash
again to log another terminal to your virtual machine!
This application is meant to keep track of your spending. Let's imagine we are going to use it to jot down what we spend, where, when, why, how, etc. We will start with a Transaction
table that is meant to store all this data. For our transaction table to work we MUST have a Transaction
model that can speak to the table so:
- create a file
app/models/transaction.rb
- add the most basic requirements to the file
class Transaction < ApplicationRecord
end
- create the migration for this table
rails generate migration create_transactions
, note that Rails is smart enough to know that we are creating a table with this migration, it will add some code to help us on our way. - add instructions that Rails will use for creating this table
class CreateTransactions < ActiveRecord::Migration[5.2]
def change
create_table :transactions do |t|
t.string :name
t.integer :amount
t.date :date
t.string :transaction_type
t.text :notes
end
end
end
- run the migration
bin/rake db:migrate
- check that everything is working as expected, start a
rails console
and your app shouldn't scream at you for doing something likeTransaction.new
orTransaction.all
In Rails console try: Transaction.create(name: "Tim Hortons", amount: 325, date: Time.current, transaction_type: 'purchase', notes: 'Donuts and coffee')
you should see something like
irb(main):002:0> Transaction.create(name: "Tim Hortons", amount: 325, date: Time.current, transaction_type: 'purchase', notes: 'Donuts and coffee')
(0.9ms) BEGIN
Transaction Create (1.6ms) INSERT INTO `transactions` (`name`, `amount`, `date`, `transaction_type`, `notes`) VALUES ('Tim Hortons', 325, '2019-01-29', 'purchase', 'Donuts and coffee')
(2.9ms) COMMIT
=> #<Transaction id: 2, name: "Tim Hortons", amount: 325, date: "2019-01-29", transaction_type: "purchase", notes: "Donuts and coffee">
So now that we know that we can create a record let's add some seed data to this table, we are basically going to do the same thing as the above command but we want a bunch of variations and to do this many times.
The following command we could just do in rails console
because there aren't that many of them. But since we want to save this to a file so that we can easily create some seed data we would add this to db/seeds.rb
like we did in assignment 3. Then we could run it by entering bin/rails db:seed
in command line. I've also added a line that says we should destroy all rows before we run the seed.
Transaction.destroy_all
Transaction.create(name: "Superstore", amount: 8243, date: 3.days.ago, transaction_type: 'purchase', notes: 'Cereal and steaks')
Transaction.create(name: "Canadian Tire", amount: 4472, date: 2.days.ago, transaction_type: 'purchase', notes: 'Engine Oil')
Transaction.create(name: "Esso", amount: 4291, date: 2.days.ago, transaction_type: 'purchase', notes: 'Fill up')
Transaction.create(name: "Walmart", amount: 1284, date: 2.days.ago, transaction_type: 'purchase', notes: 'Socks')
Transaction.create(name: "Woofies", amount: 5561, date: 2.days.ago, transaction_type: 'purchase', notes: 'Dog food')
Transaction.create(name: "Work", amount: 100000, date: 2.days.ago, transaction_type: 'deposit', notes: 'Payday')
Transaction.create(name: "Rialto Theatre", amount: 3310, date: 1.days.ago, transaction_type: 'purchase', notes: 'Date night')
Transaction.create(name: "Thrifties", amount: 2123, date: 1.days.ago, transaction_type: 'purchase', notes: 'Lunch meat')
Checkout your various records in Rails console Transaction.all
, Transaction.first
, Transaction.last
, Transaction.find(3)
good time for a git commit!
Wicked! Now I want to see something pretty on the frontend. Let's create a route for people to go to
- in your
config/routes.rb
file we want a url to go to. Addresources :transactions
- check your routes by typing in
bin/rake routes
, you should see some paths available to you now
transactions GET /transactions(.:format) transactions#index
POST /transactions(.:format) transactions#create
new_transaction GET /transactions/new(.:format) transactions#new
edit_transaction GET /transactions/:id/edit(.:format) transactions#edit
transaction GET /transactions/:id(.:format) transactions#show
PATCH /transactions/:id(.:format) transactions#update
PUT /transactions/:id(.:format) transactions#update
DELETE /transactions/:id(.:format) transactions#destroy
This shows that a GET
request to the url /transactions
will go to a controller called transactions
and call the action index
. Knowing this we can create our controller
Here's that same output a bit more clear, some assumptions are made for all requests when using resources
in routes.rb
- GET /transactions transactions#index
- POST /transactions transactions#create
- GET /transactions/new transactions#new
- GET /transactions/:id/edit transactions#edit
- GET /transactions/:id transactions#show
- PATCH OR PUT /transactions/:id transactions#update
- DELETE /transactions/:id transactions#destroy
Knowing this will help you understand why certain urls go to certain places
- create a file
app/controllers/transactions_controller.rb
- it should inherit the application controller
class TransactionsController < ApplicationController
end
Make sure your naming is exactly as I've typed. Rails is smart enough to look for your files intelligently but if they break the naming convention you're not going to have a good time.
Let's add that index
action
class TransactionsController < ApplicationController
def index
end
end
Now we can test that the controller is working, but I know that this will fail because we don't have a view yet. Let's try anyways, head to http://localhost:3000/transactions
TransactionsController#index is missing a template
- Create a directory with the same name as our controller
app/views/transactions
- Now create a file with the same name as our action
app/views/transactions/index.html.erb
, we use the.erb
format so that we can use actual ruby code inside our html file
test that you see your file at http://localhost:3000/transactions
Fill that sucker in with something pretty, remember you can use <% %>
to parse ruby code and <%= %>
to output it
<% Transaction.all.each do |transaction| %>
<div><%= transaction.name %></div>
<% end %>
good time for a git commit!
I've said this before be we want our views to be stupid, try to get in the habit of pulling logic and operations out of the view so that they only have one job to do. Your logic should go in controllers or models as much as possible.
Knowing this let's update our code to pass variables to our view from our controller
In app/controllers/transactions_controller.rb
let's update the index
action
class TransactionsController < ApplicationController
def index
@transactions = Transaction.all
end
end
and now in app/views/transactions/index.html.erb
<% @transactions.each do |transaction| %>
<div><%= transaction.name %></div>
<% end %>
Good now, our index view doesn't worry about putting together a collection of transactions, it only needs to care about rendering them. This is helpful if we only wanted to show a scoped collection of transactions, for example maybe "this months purchase transactions".
Let's add a link to our view so that we can see details about each transaction. We are going to use the link_to
helper for this.
<% @transactions.each do |transaction| %>
<div><%= transaction.name %> <%= link_to 'details', transaction_path(transaction) %></div>
<% end %>
Clicking that link should take us to a url that looks like: http://localhost:3000/transactions/1
and an error message
The action 'show' could not be found for TransactionsController
We need to update our controller and add a new view
In app/controllers/transactions_controller.rb
def show
@transaction = Transaction.find(params[:id])
end
params
is a reserved variable that Rails uses to pass arguments to the controller. Seeing the contents of params
can be a difficult if you don't know what tools to use since at this point we don't have a place to output it to (we are still in the controller and not the view). So one indication of what param
might be getting passed can be found in your bin/rake routes
output.
transaction GET /transactions/:id(.:format) transactions#show
here the :id
in /transactions/:id
shows that a url with transactions/1
will have 1
passed as the the params[:id]
If you saw something like:
my_query GET /my_url/:id/:date/:category(.:format) my_controller#my_action
We could understand that a request to the url /my_url/14/2018-05-31/bicycles
would have
params[:id]
as14
params[:date]
as2018-05-31
params[:category]
asbicycles
- controller would be
my_controller
- action would be
my_action
- method would be
GET
- rails relative path
my_query_path(id: 14, date: 2018-05-31, category: bicycles)
would output/my_url/14/2018-05-31/bicycles
- rails absolute path
my_query_url(id: 14, date: 2018-05-31, category: bicycles)
would behttp://my_host.com//my_url/14/2018-05-31/bicycles
Another tip is by looking at the terminal that your ran your rails server
command in. It shows an output of your server logs, you might see your GET
request and the params
there
Started GET "/transactions/1" for 172.17.0.1 at 2019-01-29 06:32:01 +0000
Cannot render console from 172.17.0.1! Allowed networks: 127.0.0.1, ::1, 127.0.0.0/127.255.255.255
(0.5ms) 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
↳ /usr/local/bundle/gems/activerecord-5.2.2/lib/active_record/log_subscriber.rb:98
Processing by TransactionsController#show as HTML
Parameters: {"id"=>"1"}
create a file app/views/transactions/show.html.erb
and add your output
<p><%= @transaction.name %></p>
<p><%= @transaction.amount %></p>
<p><%= @transaction.date %></p>
<p><%= @transaction.transaction_type %></p>
<p><%= @transaction.notes %></p>
good time for a git commit!
Let's add more functionality to this app, we want to have a form where we can create new transactions, we will utilize new
, create
actions for this. Remembering our bin/rake routes
output:
POST /transactions(.:format) transactions#create
new_transaction GET /transactions/new(.:format) transactions#new
Add the new
action to our app/controller/transactions_controller.rb
, it will have a new
transaction record ready for our view
def new
@transaction = Transaction.new
end
Now create the view app/views/transactions/new.html.erb
with form using the form_for
, label
, text_field
, number_field
, date_field
, and text_area
helpers
<%= 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 %>
Remembering those routes we know @transaction
has no :id
and since it's a form submission we expect a POST
request. This form will submit a POST
to /transactions/
thus hitting the create
action, so let's create that now
first things first, we need to make sure that the values coming from the browser are sanitized and cleaned, we don't want to allow any bad hacky stuff in so we are going to only allow the params
that we want.
If you happened to look at the HTML source code of the our form, you might have noticed that the form actually uses attributes that look like name="transaction[name]"
, this means that all of our params from the new
form are going to be wrapped in a param called transaction
, see here in a log of what this POST
request might look like:
Parameters: {"utf8"=>"✓", "authenticity_token"=>"3wUCzAcMLjlwlUdiCUusci+t216YFQi23bBGfvRtbWkp3PIpdkPMIflhuo0gfw6+zC4cMBHGx8UbAAMrBYX5RQ==", "transaction"=>{"name"=>"Super", "amount"=>"2", "date"=>"2019-12-31", "transaction_type"=>"purchase", "notes"=>"fdsjalkf"}, "commit"=>"Create Transaction"}
So you can see that we actually need to pull out the parameters from params[:transaction]
, we do this by using a type of white-listing called strong_parameters
. First we create a private method in our app/controller/transactions_controller.rb
, with our white-listed params
private
def transaction_params
params.require(:transaction).permit(:name, :amount, :date, :transaction_type, :notes)
end
Then we can pass that safely to our model, so let's add the create
action to our app/controller/transactions_controller.rb
def create
@transaction = Transaction.new(transaction_params)
if @transaction.save
redirect_to transaction_path(@transaction)
else
render :new
end
end
Here we want a bit of logic that says: if I save successfully let's go to the show page for the new transaction, otherwise we will go back to that form so you can fix try again
Notice we didn't actually have to make a new view for the create action since it's either getting redirected or going back to the new
view
Now let's make a couple adjustments to our views so we can navigate the site a bit more nicely
in app/views/transactions/show.html.erb
and app/views/transactions/new.html.erb
add a back to index
link
<p><%= link_to 'back to index', transactions_path %></p>
and at the top of app/views/transactions/index.html.erb
add a link to the new
form
<%= link_to "create a new transaction", new_transaction_path %>
What happens if we mess something up? we should be able to make updates to existing records!
edit_transaction GET /transactions/:id/edit(.:format) transactions#edit
PATCH /transactions/:id(.:format) transactions#update
PUT /transactions/:id(.:format) transactions#update
Following the same convention as the new
and create
actions, let's use edit
to show the form and update
to modify the record
Add the edit
action to our app/controller/transactions_controller.rb
, it will load an existing transaction record from the params[:id]
def edit
@transaction = Transaction.find(params[:id])
end
Now create the view app/views/transactions/edit.html.erb
with form using the form_for
, label
, text_field
, number_field
, date_field
, and text_area
helpers as we did last time, also may as well add that "back to index" link now.
You can literally copy and paste your app/views/transactions/new.html.erb
into app/views/transactions/edit.html.erb
now go to: http://localhost:3000/transactions/1/edit
and voila your form is there and all your fields are pre-filled!
Before we can post this form to the controller we should add the update
action in app/controller/transactions_controller.rb
def update
@transaction = Transaction.find(params[:id])
if @transaction.update(transaction_params)
redirect_to transaction_path(@transaction)
else
render :edit
end
end
So to break this down we are first finding the transaction record using the params[:id]
. Then we can use handy Rails model method update
to update the record, we simply pass the transation_params
that we white-listed earlier.
A bit of conditional logic to redirect to the show
page if the record is successfully update or go back to the edit
form if something fails
But wait, now that we can update we should add a link on the index page for each transaction record to get to it's edit form. In app/views/transactions/index.html.erb
add:
<%= link_to 'edit', edit_transaction_path(transaction) %>
Final part of this CRUD tutorial. We should be able to destroy these records if we screw up. Let's add a destroy link at the bottom of the app/views/transactions/edit.html.erb
page.
DELETE /transactions/:id(.:format) transactions#destroy
I'm sure you've dealt with GET
and POST
requests, but what's a DELETE
request? It's actually basically just a POST
request with some extra parameters. But really we can't make a link do a POST
request can we? Well we sure can if we use the Rails link_to
helper.
<%= link_to 'Destroy', @transaction, method: :delete %>
Now we just need to create the action in app/controller/transactions_controller.rb
def destroy
@transaction = Transaction.find(params[:id])
@transaction.destroy
redirect_to transactions_path
end
Nice work