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
clonedropdown 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 theoriginnamespace - you'll need to stage all the files you just created when you did the
rails newcommand. 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 consoleand your app shouldn't scream at you for doing something likeTransaction.neworTransaction.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.rbfile 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.erbformat 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]as14params[:date]as2018-05-31params[: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