Skip to content

Instantly share code, notes, and snippets.

@yosukehasumi
Last active February 5, 2019 03:36
Show Gist options
  • Save yosukehasumi/8f647bb920da80e1e1dd21e7b11558c1 to your computer and use it in GitHub Desktop.
Save yosukehasumi/8f647bb920da80e1e1dd21e7b11558c1 to your computer and use it in GitHub Desktop.
img240 Week 4 Assignment

Assignment 4

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.

Setup

Git setup

  1. create a repo project in gitlab called 'mint', this should be in your "student" repo!

Project Initialization

  1. 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

  1. in command line you'll need to change directory into your project cd mint

Git init

  1. in gitlab get your repo url, look for the clone dropdown in your project dashboard
  2. 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 the origin namespace
  3. you'll need to stage all the files you just created when you did the rails new command. In command line do git add .
  4. 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

  1. now push your changes, git push origin master

Hook up your database

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

Create a database

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'

Check it out

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.

  1. git add .
  2. git commit -m 'Rails is working!'
  3. git push origin master

Bonus

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!

Lets Build some stuff

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 Transaction model & migration

  1. create a file app/models/transaction.rb
  2. add the most basic requirements to the file
class Transaction < ApplicationRecord
end
  1. 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.
  2. 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
  1. run the migration bin/rake db:migrate
  2. check that everything is working as expected, start a rails console and your app shouldn't scream at you for doing something like Transaction.new or Transaction.all

Create a new Transaction record

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">

Seed

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!

Frontend

Wicked! Now I want to see something pretty on the frontend. Let's create a route for people to go to

Routes

  1. in your config/routes.rb file we want a url to go to. Add resources :transactions
  2. 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

  1. GET /transactions transactions#index
  2. POST /transactions transactions#create
  3. GET /transactions/new transactions#new
  4. GET /transactions/:id/edit transactions#edit
  5. GET /transactions/:id transactions#show
  6. PATCH OR PUT /transactions/:id transactions#update
  7. DELETE /transactions/:id transactions#destroy

Knowing this will help you understand why certain urls go to certain places

Create the controller

  1. create a file app/controllers/transactions_controller.rb
  2. 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.

Create the action

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 the view

  1. Create a directory with the same name as our controller app/views/transactions
  2. 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!

Smarter controllers, stupider views

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

Adding a show action

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

  1. params[:id] as 14
  2. params[:date] as 2018-05-31
  3. params[:category] as bicycles
  4. controller would be my_controller
  5. action would be my_action
  6. method would be GET
  7. rails relative path my_query_path(id: 14, date: 2018-05-31, category: bicycles) would output /my_url/14/2018-05-31/bicycles
  8. rails absolute path my_query_url(id: 14, date: 2018-05-31, category: bicycles) would be http://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"}

Adding a show view

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!

Part II - Create Form

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 %>

Part III - Edit and Update

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) %>

Part IV - Destroy

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

FIN

Nice work

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