Professional Documents
Culture Documents
of Contents
1. Introduction 1.1
1. Learning objectives 1.1.1
2. Your first website 1.2
3. ATM Challenge - Ruby basics 1.3
1. Step 1 1.3.1
2. Step 2 1.3.2
3. Step 3 1.3.3
4. Step 4 1.3.4
5. Step 5 1.3.5
6. Step 6 1.3.6
7. Step 7 1.3.7
8. Step 8 1.3.8
9. Step 9 1.3.9
10. Step 10 1.3.10
4. Library challenge - Advanced Ruby 1.4
1. Important topics 1.4.1
5. Javascript Introduction 1.5
1. Variables, objects and arrays 1.5.1
2. Comparisons and Manipulations 1.5.2
3. Javascript Sample Problems 1.5.3
4. Defining Functions 1.5.4
5. Prototypes & Classes 1.5.5
6. Miscellaneous 1.5.6
6. BMI Challenge - JavaScript basics 1.6
1. Jasmine - Set up 1.6.1
2. First tests 1.6.2
3. The calculator 1.6.3
4. The Document-Object Model 1.6.4
5. Web interface 1.6.5
6. Acceptance tests 1.6.6
7. Moving on 1.6.7
7. Fizz Buzz in JavaScript 1.7
1. NodeJS 1.7.1
8. Checkout challenge 1.8
9. Open Weather Challenge 1.9
10. SlowFood challenge - OO & TDD 1.10
1. Step 1 - Setting up the project 1.10.1
2. Step 2 - Focus on the user experience 1.10.2
3. Step 3 - Entity Relationship Diagrams 1.10.3
4. Step 4 - Implementing the core features 1.10.4
5. Step 5 - Working with the database 1.10.5
6. Step 6 - Working with BDD 1.10.6
7. Extra - Setting up RSpec & Cucumber 1.10.7
11. Static Website with Middleman 1.11
1. Week lab 1.11.1
2. Setup Middleman 1.11.2
3. HAML - HTML abstraction markup language 1.11.3
4. SASS 1.11.4
5. Accessing data 1.11.5
2
12.
13.
14.
15.
16.
17.
18.
19.
20.
6. Partials 1.11.6
7. Deploy to Github pages 1.11.7
Ruby On Rails introduction 1.12
BDD with Rails 1.13
Rails Messaging 1.14
1. Working with Legacy Code 1.14.1
2. Tips and Tricks 1.14.2
Mid Course Project 1.15
1. Project Schedule 1.15.1
Going mobile with Ionic 1.16
1. Getting started 1.16.1
2. Cleaning up and adding views 1.16.2
3. Adding the calculator tab 1.16.3
4. Adding functionality 1.16.4
5. Extras - Source code 1.16.5
The Cooper test challenge 1.17
1. The logic 1.17.1
2. The Back-end 1.17.2
3. The Client 1.17.3
4. Connecting the dots 1.17.4
5. Saving and retrieving data 1.17.5
6. Display charts 1.17.6
7. Wrapping up 1.17.7
8. Results tables 1.17.8
SlowFood Online Challenge - Hello World! 1.18
1. Main features 1.18.1
2. Design Sprint 1.18.2
3. Pivotal Tracker 1.18.3
SlowFood API - API first or second? 1.19
Extras 1.20
1. Naming Standards 1.20.1
2. Classes vs Modules 1.20.2
3. Code structure 1.20.3
4. Bower 1.20.4
5. Code Review Instructions 1.20.5
6. About README's 1.20.6
7. MVC 1.20.7
8. Three-Tier Architecture 1.20.8
9. AngularJS 1.20.9
Introduction
Introduction
Craft Academy Bootcamp
Coding as a Craft - in 12 weeks
Craft Academy is a 12-week intensive, practice-oriented course for aspiring junior developers. The
course is delivered as a coding bootcamp by Craft Academy in Gothenburg, Sweden. It gives you the
basics you need to create, develop and launch web-based applications.
The concept of Craft Academy Bootcamp is simple, it is a 12 week intensive coding camp, aimed at
teaching you the fundamentals of web development, enabling you to hit the ground running and
keep running. But what is important is that modern engineering practices are considered to be part of
this foundation. As a student, you work from day one with automated testing, continuous
integration and deployment, and other essential skills. Meaning that when you enter the workforce
and are faced with problem, your instinct will be to solve right rather than just hacking a solution
together.
Open Source
This publication contains the course materials we ask all our future students to go through. You are
free to use it for your personal learning needs even if you are not attending our bootcamp, we
encourage that.
It is however not allowed to use this material in a commercial context without our written
consent. It is not okay and it is against the very meaning of open source.
If you have any questions, feel free to contact us at info@craftacademy.se
Thomas Ochman
Gothenburg, April 2016
www.craftacademy.se
Craft Academy Bootcamp by Pragmatic Sweden is licensed under a Creative Commons AttributionNonCommercial 4.0 International License.
Learning objectives
Learning objectives
Learning objectives
The 4 streams of skills
Technology
We are a coding camp - so one of the most tangible results of your participation in Craft Academy
will be an ability to write code. We will cover different set of programming languages, frameworks
and...
Agile methodology
Collaboration
Business orientation
In your terminal, go to the folder where you want to store your project, and clone the new repository:
$ git clone https://github.com/username/username.github.io
$ cd username.github.io
$ echo "<h1>Hello World</h1>" > index.html
$ echo "<p>I'm Thomas, and I attend the Craft Academy Bootcamp</p>"
>> index.html
Use Git to commit, and push your changes to GitHub:
$ git add .
$ git commit -m "initial commit"
$ git push origin master
Open your browser and go to username.github.io.
You have just deployed your first web site.
The challenge
Our client is a financial institution that wants to allow its customers to withdraw funds from their
accounts using an Automatic Teller Machine (ATM). They have turned to us for a prototype of a
system with limited functionality. Our job is to write a simple Ruby program that can be run in the
Interactive Ruby Shell (IRB).
You will be working with your Coach and your peers to get started with using RSpec as a
testing framework an with implementing your code.
Scope
The following objectives must be met:
An ATM machine can hold up to $1000
Withdrawal can be cleared only if
The ATM holds enough funds
The amount is divisible by 5
the person attempting the withdrawal provides a valid ATM card
Valid pin and expire date
Card status must be active (Not report stolen or lost)
the person attempting the withdrawal has sufficient funds on his account
There are only $5, $10 and $20 bills in the ATM. Withdrawals for amounts not divisible by 5
must be rejected.
Upon a successful withdrawal the system should return a receipt with information about the
date, amount and bills that was dispatched. (The receipt should be presented in the form of a
Hash
Example output
# sucessful withdrawal
{ status: true, message: 'success', date: '2016-01-30', amount: 35,
bills: [20,10,5]}
# wrong pin
{ status: false, message: 'wrong pin', date: '2016-01-30'}
9
# expired card
{ status: false, message: 'card expired', date: '2016-01-30'}
10
Step 1
Step 1
Step 1 - Setting the stage
The first thing we need to do is to set up the necessary tools we'll be using. We know that we'll be
using Ruby as the programming language. That is already set up on our system.
We also know that we'll be trying to write our application using Test Driven Development - or at least
try to do that. For that we'll need a testing framework. Enter RSpec - the most frequently used testing
library for Ruby applications. Even though it has a very rich and powerful DSL (domain-specific
language), at its core it is a simple tool which you can start using rather quickly.
In order to be able to use it we need to install it. There are two ways to install libraries (gems). A
direct install from your terminal (gem install rspec) or by adding a gem as a dependency to
your application using Bundler. It is pretty simple, you just add a gem to a specific file named
Gemfile.
Let's do that.
Create a new Gemfile from your terminal in the folder that you want to use for your application.
$ touch Gemfile
Add the following content to that file.
!FILENAME Gemfile
source 'https://rubygems.org'
gem 'rspec'
Save and head over to your terminal window and run the bundle install command.
If you get an error message and the system complains about not finding Bundler, just run this
command to install it.
$ gem install bundler
And run bundle install again.
That installs RSpec.
The next step is to initialize RSpec and configure it for our needs.
$ rspec --init
Edit the .rspec file and add --format documentation to see a more verbose rspec output. Your
.rspec file needs to look like this.
!FILENAME .rspec
--format documentation
11
Step 1
--color
--require spec_helper
Now, if you go back to your terminal and run the rspec command, you should see something like this.
$ rspec
No examples found.
Finished in 0.00028 seconds (files took 0.40297 seconds to load)
0 examples, 0 failures
Alright, that means we are set and ready to test.
Using Git
Let me put down some ground rules about version control. Commit often, write good commit
messages and push up to your GitHub account. That is the only way for us coaches to see your
progress. It does not matter if the code is working. We still want to see it. Bad code is better then No
code!
At this stage you need to set up a git repository. I suggest that you create a GitHub repository, copy
the address and add it as a remote to your local repository (We are about to create one).
In your terminal, initialize a new git repository with the init command.
$ git init
Initialized empty Git repository in /your/path/atm/.git/
~/your/path/atm [master|2]
Next, you need to create a .gitignore file. That file is used to keep information about files we
want to EXCLUDE from version control.
$ touch .gitignore
Add at this to that file.
!FILENAME .gitignore
.DS_Store
.DS_Store (Desktop Services Store) is a OSX file that stores custom attributes of its containing
folder, such as the position of icons or the choice of a background image. We don't want to track those
files with git.
Now, perform the following steps.
$ git remote add origin <your git repo url>
$ git add .
$ git commit -am "<your message>"
$ git push origin master
12
Step 2
Step 2
Step 2 - The core functionality
My approach to writing new software is to always do the most important thing first and get it done
before I move on to other, less important, functions. What is the core functionality of this application?
I would argue that creating an ATM that has some funds is the first thing that we should focus on. If
there is no ATM you will not be able to do a withdrawal, right? And it the ATM has no funds you
won't be able to get any cash from it either.
So let's start with creating a ATM class and assign some funds to each ATM that we create. You already
know a little bit about classes from the Prep Course material.
Since we are working with TDD, we start with creating a test file first.
As a reminder, It is important that we agree on three things at this point.
Your tests/specs are placed in the spec folder
Your implementation (or production code) are placed in the lib folder
Your settings (like Gemfile, etc.) are placed in the main project folder
Alright?
Okay, moving on... Create a new file named atm_spec.rb in your spec folder.
$ touch spec/atm_spec.rb
Let's add the following test to that file. Note the keywords describe and it. Also, as in all ruby
programs we are creating blocks with the do and end keywords. Make it a habit that you always add
an end if you type do.
!FILENAME spec/atm_spec.rb
require './lib/atm.rb'
describe Atm do
it 'has 1000$ on intitialize' do
expect(subject.funds).to eq 1000
end
end
Make sure that you run that spec from your terminal.
$ rspec spec/atm_spec.rb
What will follow now is a series of steps that aims at showing you how testing can drive your
development. In the future you will probably skip some of this steps but for now, bear with me.
If you examine the terminal output, you'll see a line like this one.
/your/path/atm/spec/atm_spec.rb:1:in `require': cannot load such fil
e -- ./lib/atm.rb (LoadError)
...
13
Step 2
That means that the spec file can not load ./lib/atm.rb (where we are supposed to have our
implementation code).
Of course not, we haven't created that file yet. There's no lib folder yet either. Let's create all that
now.
$ mkdir lib
$ touch lib/atm.rb
Run your spec again.
rspec spec/atm_spec.rb
/your/path/atm/spec/atm_spec.rb:2:in `<top (required)>': uninitializ
ed constant Atm (NameError)
A new error message. But not the same as before. That is good. So what have we here?
uninitialized constant Atm? Yes, there is no Atm class defined. Let's do that.
!FILENAME lib/atm.rb
class Atm
end
Let's have another go at the spec.
$ rspec spec/atm_spec.rb
Atm
has 1000$ on initialize (FAILED - 1)
Failures:
1) Atm has 1000$ on initialize
Failure/Error: expect(subject.funds).to eq 1000
NoMethodError:
undefined method `funds' for #<Atm:0x007f8043fdf2a8>
New error message? Cool!
Yes, there is no method funds for the Atm class. Let's add that by adding a attr_accessor
:funds to the class. What is attr_accessor? You can read about it in this Stack Overflow
answer.
!FILENAME lib/atm.rb
class Atm
attr_accessor :funds
end
Another go at the spec and another error message.
$ rspec spec/atm_spec.rb
14
Step 2
Atm
has 1000$ on intitialize (FAILED - 1)
Failures:
1) Atm has 1000$ on intitialize
Failure/Error: expect(subject.funds).to eq 1000
expected: 1000
got: nil
Okay, so we expected funds to be 1000 but it was nil. Let's make it so that every time an ATM
object is instantiated the balance is automatically set to 1000.
We can do that by setting that value in the initialize method. initialize is a constructor
method that will be run every time an instance of a class is created.
!FILENAME lib/atm.rb
class Atm
attr_accessor :funds
def initialize
@funds = 1000
end
end
And now, when you run RSpec, the test passes.
$ rspec spec/atm_spec.rb
Atm
has 1000$ on initialize
Finished in 0.00195 seconds (files took 0.67858 seconds to load)
1 example, 0 failures
Yay! First success! Green is GOOD!
Lesson learned: Every feature, no matter how small, will lead to a series of failures. Until it doesn't.
This goes for new, inexperienced programmers, as well as for those of us who has been doing this for
a long time. There's nothing wrong with you. Just get used to it and see everything you do as a
learning experience.
Keep your calm, read the error messages that RSpec so kindly throws at you and make small steps
forward. Be thankful that you have a testing framework that helps you to figure out what is wrong
with your code. Imagine if you were coding without it?
Alright, enough of coding philosophy. Let's move on. This is a great time to do a commit and push up
your code (Unless you already did that).
Doing a withdrawal
Let's add another test to the atm_spec. Inside the describe Atmblock, add this spec.
15
Step 2
!FILENAME spec/atm_spec.rb
it 'funds are reduced at withdraw' do
subject.withdraw 50
expect(subject.funds).to eq 950
end
In my spec file that it block starts at line 7. What I can do is to run JUST that particular block,
instead of the entire spec file. It might seem trivial right now, but further down the road we'll have
dozens of specs and trust me, you don't want to keep running them all at once.
$ rspec spec/atm_spec.rb:7
Run options: include {:locations=>{"./spec/atm_spec.rb"=>[7]}}
Atm
funds are reduced at withdraw (FAILED - 1)
Failures:
1) Atm funds are reduced at withdraw
Failure/Error: subject.withdraw 50
NoMethodError:
undefined method `withdraw' for #<Atm:0x007fac30e79378 @balan
ce=1000>
...
Shoots! New error. Yes, yes, yes. That is supposed to happen! ;-)
So, we have an undefined method 'withdraw'. Alright. let's create the withdraw method
and let it take one argument - the amount we want to withdraw from the Atm.
!FILENAME lib/atm.rb
class Atm
attr_accessor :funds
def initialize
@funds = 1000
end
def withdraw(amount)
end
end
Run RSpec just to see another error message.
$ rspec spec/atm_spec.rb:7
Run options: include {:locations=>{"./spec/atm_spec.rb"=>[7]}}
Atm
funds are reduced at withdraw (FAILED - 1)
Failures:
16
Step 2
17
Step 3
Step 3
Step 3
Okay, so we have a basic withdraw method for our Atm class. It's a good start. Now, if we have a
look at the requirements we initially got from our client, we see that a successful withdraw should
generate a response in the form of a Hash.
This hash is the equivalent of a receipt that the Atm prints out in the real life. It should look like
this if the transaction was successful:
{ status: true, message: 'success', date: '2016-01-30', amount: 35,
bills: [20,10,5]}
For unsuccessful transactions, it should look like this:
{ status: false, message: '[reason for failure e. e. wrong pin]', da
te: '2016-01-30'}
Let's break this down.
status
Can be true or false depending if the transaction was successful.
message
A message to the user. We can set that to success when the transaction was successful and to
something else if we for some reason can not perform the transaction.
date
The date of the transaction - simply today's date.
amount
Visible only when transaction was successful.
Simply the amount that was withdrawn.
bills
Visible only when transaction was successful.
An array of bills that was dispatched by the ATM. This symbolize the actual cash you would
get in real life.
Step 3
The ATM needs to interact with another class - we will call it Account. The Account class will
symbolize both the bank account and a card we can use in the ATM (there is no need to create both an
Account class and a Card class for the sake of this prototype).
However, we have not created that class yet, so in out atm_spec we will use a so called
instance_double in order to be able to test the functionality. Doubles are objects that can be
used as stand-ins for instances of other classes (hence the name instance_double). Even if they
still are not defined (as in our case). We will go over doubles more extensively further down the road
in the camp. You can think of doubles as "fake" objects that we use for testing. We don't want to build
the Account class yet, so we'll just make a fake one for now.
Let's define a class_double in our spec and give it a name of account. We'll give our
account a @balance of 100. Then we'll be able to use this in our testing.
!FILENAME spec/atm_spec.rb
describe Atm do
let(:account) { instance_double('Account') }
before do
# Before each test we need to add an attribute of `balance`
# to the `account` object and set the value to `100`
allow(account).to receive(:balance).and_return(100)
# We also need to allow `account` to receive the new balance
# using the setter method `balance=`
allow(account).to receive(:balance=)
end
[...]
end
Okay, we want the withdraw method to have access to the account object in order to know things
about it. Things like a balance for instance, right? The ATM needs to know if there are enough
funds in the account before it clears the transaction.
First we will write a test and then we will modify the implementation code.
Note: Make sure that you read the comments in the it block below but do not include them in your
spec.
!FILENAME spec/atm_spec.rb
describe Atm do
[...]
it 'allow withdraw if account has enough balance.' do
# We need to tell the spec what to look for as the responce
# and store that in a variable called `expected_outcome`.
# Please note that we are omitting the `bills` part at the momen
t,
# We will modify this test and add that later.
expected_output = { status: true, message: 'success', date: Date
.today, amount: 45 }
# We need to pass in two arguments to the `withdraw` method.
19
Step 3
Step 3
end
[...]
Now, everything should go green when you run your tests.
21
Step 4
Step 4
Step 4 - Refactoring
Okay, we will stop here for a moment ad do some changes to our code to make it more readable and
to follow the principle that each method should only have one responsibility. In our case, the way we
have written the withdraw method, the method perform several tasks. Since we will be developing
that method further, we want to introduce a better, more readable structure.
In practical terms, we want to extract some of this methods responsibilities to separate, so called
private methods.
1. We want to extract the check of account.balance to a separate method.
2. We want to extract the transaction to a separate method.
Evaluate this code carefully.
!FILENAME lib/atm.rb
class Atm
attr_accessor :funds
def initialize
@funds = 1000
end
def withdraw(amount, account)
# We will be using Ruby's `case`- `when` - `then` flow control s
tatement
# and check if there is enough funds in the account
case
when insufficient_funds_in_account?(amount, account)
# we exit the method if the amount we want to withdraw is bigg
er than
# the balance on the account
return
else
# If it's not, we perform the transaction
perform_transaction(amount, account)
end
end
private
def insufficient_funds_in_account?(amount, account)
amount > account.balance
end
def perform_transaction(amount, account)
# We DEDUCT the amount from the Atm's funds
22
Step 4
@funds -= amount
# We also DEDUCT the amount from the accounts balance
account.balance = account.balance - amount
# and we return a responce for a successfull withdraw.
{ status: true, message: 'success', date: Date.today, amount: am
ount }
end
end
Note that we have NOT made any changes to our test and if you run them now, they should all pass
green.
Refactoring is all about that. You make your code better WITHOUT introducing any new
functionality.
23
Step 5
Step 5
Step 5 - Testing the sad path
Okay, so now we can do a withdrawal IF there is money in the account.
Let's start thinking about everything that can go wrong with a withdrawal.
For starters, what if there is not enough money in the account? At this point we only return from
the method without any feedback for the user.
Let's change that.
!FILENAME spec/atm_spec.rb
[...]
it 'rejects withdraw if account has insufficient funds' do
expected_output = { status: false, message: 'insufficient funds',
date: Date.today }
# We know that the account created for the purpose of this test
# has a balance of 100. So let's try to withdraw
# a larger amount. In this case 105.
expect(subject.withdraw(105, account)).to eq expected_output
end
[...]
This test should fail for you.
The following implementation is needed.
!FILENAME lib/atm.rb
def withdraw(amount, account)
case
when insufficient_funds_in_account?(amount, account)
{ status: false, message: 'insufficient funds', date: Date.today
}
else
perform_transaction(amount, account)
end
end
24
Step 6
Step 6
Step 6 - More checks
Another check we need to do in the withdraw method is to see if there are funds in the ATM, right?
We can not perform a transaction if there are no funds in the machine.
The ATM has a funds attribute. We can perform a check if the amount we try to withdraw is larger
then the funds available.
Let's add a spec for that.
!FILENAME spec/atm_spec.rb
[...]
it 'reject withdraw if ATM has insufficient funds' do
# To prepare the test we want to decrease the funds value
# to a lower value then the original 1000
subject.funds = 50
# Then we set the `expected_output`
expected_output = { status: false, message: 'insufficient funds in
ATM', date: Date.today }
# And prepare our assertion/expectation
expect(subject.withdraw(100, account)).to eq expected_output
end
And implement a new when in the withdraw method.
!FILENAME lib/atm.rb
[...]
def withdraw(amount, account)
case
when insufficient_funds_in_account?(amount, account)
{ status: false, message: 'insufficient funds in account', date:
Date.today }
when insufficient_funds_in_atm?(amount)
{ status: false, message: 'insufficient funds in ATM', date: Date.to
day }
else
[...]
And, we also need to create a new private method, just as we did with the previous example.
!FILENAME lib/atm.rb
[...]
private
def insufficient_funds_in_atm?(amount)
@funds < amount
end
25
Step 6
Step 6
!FILENAME lib/atm.rb
[...]
private
def incorrect_pin?(pin_code, actual_pin)
pin_code != actual_pin
end
Expired card
Let's tackle the check for card expiration date.
First, let's modify our double.
!FILENAME spec/atm_spec.rb
[...]
let(:account) { instance_double('Account', pin_code: '1234', exp_dat
e: '04/17') }
[...]
And, as always, we write a test. (I will not include comments. By now you know what we need to do
to build a test)
!FILENAME spec/atm_spec.rb
it 'reject withdraw if card is expired' do
allow(account).to receive(:exp_date).and_return('12/15')
expected_output = { status: false, message: 'card expired', date:
Date.today }
expect(subject.withdraw(6, '1234', account)).to eq expected_output
end
And again, we need to modify the withdraw method.
!FILENAME lib/atm.rb
[...]
def withdraw(amount, account)
case
[...]
when card_expired?(account.exp_date)
{ status: false, message: 'card expired', date: Date.today }
else
[...]
Now, the method card_expired? is a little tricky. We need to make use of Ruby's Date object.
account.exp_date is of String class. We need to transform it to a Date object and compare it to
today's date. Examine the following implementation closely before implementing it.
!FILENAME lib/atm.rb
[...]
27
Step 6
def card_expired?(exp_date)
Date.strptime(exp_date, '%m/%y') < Date.today
end
Can you understand what we are doing here?
More checks
It is time for you to start to write code on your own. There is yet another one check we need to
perform. The account_status attribute will tell us if an account is active or disabled.
Our class_double will be updated with this attribute to look like this.
!FILENAME spec/atm_spec.rb
[...]
let(:account) { instance_double('Account', pin_code: '1234', exp_dat
e: '04/17', account_status: :active) }
[...]
Things to you to consider (in random order)
Note that we are using a Symbol rather than a String to set account_status.
You need to write a test for what happens if an account is :disabled
You need to update the output of every test that assumes that withdrawal was successful.
You are on your own here. If you are unsure on how to proceed make sure to go over the
methods we've already created. This is a highly repetitive process at the moment. All the
answers lies in in front of you. ;-) Good luck!
28
Step 7
Step 7
Step 7 - Cash is King
Yeah, remember when we kind of skipped adding bills to our successful output? We can't ignore that
requirement any longer. After all, the cash is the reason we use ATM's.
A part of the output our program is supposed to return on successful withdrawal, is an array of bills.
We also know that the ATM holds 5, 10 & 20$ bills, right?
Another thing we know is that one of the conditions for a successful transaction is that the amount a
user can withdraw is divisible by 5.
Knowing all this, we can build a method that tells us what bills we will get from the ATM.
Let's try some stuff out in irb. As always, read the comments as carefully as anything else in this
documentation.
18:05 $ irb
# create an array of denominations. We need to start with the bigges
t value.
# want to know why? Try the other way around for yourself.
2.2.1 :001 > denominations = [20, 10, 5]
=> [20, 10, 5]
# create an embty array and store it in a variable called `bills`
# this object will be populated with our bills.
2.2.1 :002 > bills = []
=> []
# choose an arbitrary amount - remember that we need to stick to the
# business objective. The amount needs to be divisible by 5.
2.2.1 :003 > amount = 65
=> 65
# here comes the tricky part with the `while` loop.
# for each value in the `denominations` array, we subtract it
# from `amount` until amount is lower than zero.
# at the same time we `push` the value into the `bills` array.
2.2.1 :004 > denominations.each do |bill|
2.2.1 :005 > while amount - bill >= 0
2.2.1 :006?> amount -= bill
2.2.1 :007?> bills << bill
2.2.1 :008?> end
2.2.1 :009?> bills
2.2.1 :010?> end
=> [20, 10, 5]
# and now, if we check the `bills` array, we can see that there are
4 positions.
29
Step 7
Step 7
Step 7
32
Step 8
Step 8
Step 8 - The Account
Now that we are finished (at least for now) with the Atm class, we should move forward and create
the Account class.
We will go over the same steps as we did when creating the Atm class.
1. create a test file (account_spec.rb) and
2. create a implementation file (account.rb)
In the spec file we'll be writing our spec for code that we are planning to implement. As usual there
are decisions about what attributes to add to the class and what methods needs to be defined.
Fortunately, we've already have much of the necessary
information about that class to make those decisions easier.
Remember the class_double we used while testing the Atm's functionality? If we examine that
object closely we can see that the attributes we used ware :pin_code, :balance,
:account_status and :exp_date. At the very least these are the attributes we need to assign
to the Account class definition.
There is one thing that the Account should have, that was never needed in the interaction wit Atm
but that should be a part of it. That is an account owner.
We'll let you code on your own but provide an recording that showcase a possible workflow.
Some tips for testing.
A Pin number is generally a 4 digit number. One way to check if a number has 4 digits is this.
it 'check length of a number' do
number = 1234
number_length = Math.log10(number).to_i + 1
expect(number_length).to eq 4
end
Tips: Try running parts of this in IRB if you wonder why the Math.log10 method can be used for
this and why we need to call .to_i and + 1 on the result.
Also, I would suggest that we randomize the pin code when we initialize a new Account object. A 4
digit number can be randomly generated with:
rand(1000..9999)
33
Step 8
atm_account_pi_balance
0:00 / 8:17
Expiry date
The expiry date on atm cards (and other credit cards) is generally stored in the format of month/year
- like "04/16" that translates to April 2016.
When we set the :exp_date we need to make the calculation of today's date and add a predefined
amount of years that we want the card to be valid for (remember that for the purpose of this exercise,
the account symbolizes BOTH a bank account AND an atm card)
Let's write a test to see if the :exp_date is set in initialize.
!FILENAME spec/account_spec.rb
it 'is expected to have an expiry date on initialize' do
# Here we set the validity of the card to 5 yrs as default
expected_date = Date.today.next_year(5).strftime("%m/%y")
expect(subject.exp_date).to eq expected_date
end
So, how can we implement a method to set an expiry date when an account object is created?
First of all, we need to tell the class that the default validity of the card is 5 yrs. To do that, we can set
a type of variable called constant with the default value of how many years a new card should be valid
for. It can look something like this.
class Account
STANDARD_VALIDITY_YRS = 5
end
(I would like you to find out why I use capital letters in the variable name and find out what we mean
when we say that you need to avoid Magic Numbers in your code.)
When we set the :exp_date value, we can start with today's date and add the value of the constant
and finish off by formatting the date the way we want it. It could look something like this.
def set_expire_date
34
Step 8
Date.today.next_year(Account::STANDARD_VALIDITY_YRS).strftime('%m/
%Y')
end
(Do you really need Account::STANDARD_VALIDITY_YRS? Perhaps
STANDARD_VALIDITY_YRS in enough? Try it out...)
Account status
Another check that the Atm does on the Account is to check the :account_status. That will be
the next thing we implement on the class.
We start with a spec.
!FILENAME spec/account_spec.rb
it 'is expected to have :active status on initialize' do
expect(subject.account_status).to eq :active
end
Notice that we are using the datatype Symbol to set the :account_status.
And again we need to set that attribute in our initialize method.
!FILENAME lib/account.rb
def initialize
[...]
@account_status = :active
end
Okay, so an account has status of :active when it is instantiated (created) . But how about if we
would like to deactivate an account. We could simply set the value of the :account_status
attribute to :deactivated. But we can (and should) create a method for that. The question is if we
should create a Class method or an Instance method. You need to research the difference between
this type of methods but consider this two different approaches.
def self.deactivate(account)
account.account_status = :deactivated
end
def deactivate
@account_status = :deactivated
end
Examine the two following ways of using the methods above.
it 'deactivates account using Class method' do
Account.deactivate(subject)
expect(subject.account_status).to eq :deactivated
end
it 'deactivates account using Instance method' do
subject.deactivate
35
Step 8
expect(subject.account_status).to eq :deactivated
end
Use the one that you find best in your implementation but please be ready to make an argument
about your choice.
Step 8
# http://stackoverflow.com/a/4252945
obj == nil ? missing_owner : @owner = obj
end
def missing_owner
raise "An Account owner is required"
end
37
Step 9
Step 9
Step 9 - The Person
At this step we want to create a Person class and give him 3 attributes: :name, :cash and
:account. We will give you a basic set of specs for that class. Your job will be to implement the
code that will make these specs pass.
Remember, you are free to modify these specs if you find any flaws in it OR if you find another way
of testing the same behavior.
!FILENAME spec/person_spec.rb
require './lib/person'
require './lib/atm'
describe Person do
subject { described_class.new(name: 'Thomas') }
it 'is expected to have a :name on initialize' do
expect(subject.name).not_to be nil
end
it 'is expected to raise error if no name is set' do
expect { described_class.new }.to raise_error 'A name is require
d'
end
it 'is expected to have a :cash attribute with value of 0 on initi
alize' do
expect(subject.cash).to eq 0
end
it 'is expected to have a :account attribute' do
expect(subject.account).to be nil
end
describe 'can create an Account' do
# As a Person,
# in order to be able to use banking services to manage my funds,
# i would like to be able to create a bank account
before { subject.create_account }
it 'of Account class ' do
expect(subject.account).to be_an_instance_of Account
end
it 'with himself as an owner' do
expect(subject.account.owner).to be subject
end
38
Step 9
end
describe 'can manage funds if an account been created' do
let(:atm) { Atm.new }
# As a Person with a Bank Account,
# in order to be able to put my funds in the account ,
# i would like to be able to make a deposit
before { subject.create_account }
it 'can deposit funds' do
expect(subject.deposit(100)).to be_truthy
end
describe 'can not manage funds if no account been created' do
# As a Person without a Bank Account,
# in order to prevent me from using the wrong bank account,
# I should NOT be able to to make a deposit.
it 'can\'t deposit funds' do
expect { subject.deposit(100) }.to raise_error(RuntimeError, '
No account present')
end
end
end
Making all of these tests pass will bring you closer to completing this challenge.
39
Step 10
Step 10
Step 10 - Integrating all parts
Alright, at this stage we can create an Atm, we can create a Person that has an Account. The
Personcan have cash in pocket or hold his money in his Account. All pretty straight forward.
Now we want to create a method that allows a person to withdraw funds from a specific atm and
when he does that 3 things should happen:
1. The balance of the account should DECREASE
2. The funds in the ATM should DECREASE
3. The cash in pocket should INCREASE
Consider these specs.
!FILENAME spec/person_spec.rb
describe 'can manage funds if an account been created' do
[...]
it 'funds are added to the accounst balance - deducted from cash'
do
subject.cash = 100
subject.deposit(100)
expect(subject.account.balance).to be 100
expect(subject.cash).to be 0
end
it 'can withdraw funds' do
command = lambda { subject.withdraw(amount: 100, pin: subject.ac
count.pin_code, account: subject.account, atm: atm) }
expect(command.call).to be_truthy
end
it 'withdraw is expected to raise error if no ATM is passed in' do
command = lambda { subject.withdraw(amount: 100, pin: subject.ac
count.pin_code, account: subject.account) }
expect { command.call }.to raise_error 'An ATM is required'
end
it 'funds are added to cash - deducted from account balance' do
subject.cash = 100
subject.deposit(100)
subject.withdraw(amount: 100, pin: subject.account.pin_code, acc
ount: subject.account, atm: atm)
expect(subject.account.balance).to be 0
expect(subject.cash).to be 100
end
end
Note: There are some new commands and techniques in the code above. Google them, talk to your
40
Step 10
peers and figure out WHY we are using them so you get at good understanding of what we are doing.
I will be going over some of them in my talks and break-out sessions, but it is up to you to find the
best way of using them.
Now I will show you how I implemented the Person class - at least in the first development cycle
(there are plenty of room for refactoring and I expect you to improve on this code).
!FILENAME lib/person.rb
require './lib/account'
class Person
attr_accessor :name, :cash, :account
def initialize(attrs = {})
@name = set_name(attrs[:name])
@cash = 0
@account = nil
end
def create_account
@account = Account.new(owner: self)
end
def deposit(amount)
@account == nil ? missing_account : deposit_funds(amount)
end
def withdraw(args = {})
@account == nil ? missing_account : withdraw_funds(args)
end
private
def deposit_funds(amount)
@cash -= amount
@account.balance += amount
end
def withdraw_funds(args)
args[:atm] == nil ? missing_atm : atm = args[:atm]
account = @account
amount = args[:amount]
pin = args[:pin]
response = atm.withdraw(amount, pin, account)
response[:status] == true ? increase_cash(response) : response
end
def increase_cash(response)
@cash += response[:amount]
end
41
Step 10
def set_name(name)
name == nil ? missing_name : name
end
def missing_name
raise ArgumentError, 'A name is required'
end
def missing_account
raise RuntimeError, 'No account present'
end
def missing_atm
raise RuntimeError, 'An ATM is required'
end
end
42
Learning objective
This challenge will provide you with an opporunity to practice a lot of your newly acquired technical
skills.
Practice TDD to drive your development process
Adopt to a new domain
Work with data stored in hashes and perform
Learn about YAML
Learn about storing information in a text file
Practice writing documentation
Requirements
Write a Library program with the following user stories:
As an individual
In order to get my hands on a good book
I would like to see a list of books currently available in the libra
ry
with information about the title and author
As a library
In order to have good books to offer to the public
I would like to be able to have a collection of books stored in a fi
le
As a library
In order to have good books to offer to the public
I would like to be able to allow individuals to check out a book
43
As a library
In order to make the books available to many individuals
I would like to set a return date on every check out
and I would like that date to be 1 month from checkout date
As an individual
In order to avoid awkward moments at the library
I would like to know when my book is supposed to be returned
Tasks
Fork the challenge repo: https://github.com/CraftAcademy/library-challenge
Run the command 'bundle' in the project directory to ensure you have all the gems
Write your specs and implementation
Be smart about using Git: commit and push often. Use feature branches.
Create a Pull Request as soon as possible
Read the comments from Hound and fix any issues that the service points out.
Tips
Some hints:
A Person needs to have a list of books that he currently has in his possession. That list needs to
include the return date.
The return date can be calculated using the Date object. Out of the box, there are methods you
can use to add days to the current date.
Make use of doubles when writing your specs
Follow the naming conventions/standards for methods and variables
You can take a problem set and write a well tested implementation on your own.
You understand how to define Ruby Classes and work with objects.
You understand how classes can interact with each other.
You know how to make use of arrays, hashes, and associated methods to create dynamic lists.
You know how to write specs and use them as a blueprint in your development.
I can track your work by following you commit history - so please commit as soon you are
done with a feature or when you have made a test pass.
In your Pull Request, I'm hoping to see:
That you are testing the right thing in the right spec file.
That all tests passing - green is good!
High test coverage (above 95% is accepted)
The code is easy to follow: every class has a clear responsibility, methods are short, code is
nicely formatted, etc.
The README.md includes information on how to use your solution with command examples
in irb.
44
Happy coding!
45
Important topics
Important topics
Important topics
A person needs to have an attribute where we can store books/items he or she has checked out from
the library (I called it book_shelf).
The flow of checking out an item is
1. Search for the item in library
2. Check out the item
When an item is checked out we need to store information about it in the book_shelfwith a return
date.
What is YAML?
YAML is a file format for storing object trees and data serialization which is both human readable and
computationally powerful.
All the items in the library must be stored in a yml file. The file feeds to be structured this way.
!FILENAME lib/data.yml
--- :item:
:title: Alfons och soldatpappan
:author: Gunilla Bergstrm
:available: true
:return_date:
- :item:
:title: Skratta lagom! Sa pappa berg
:author: Gunilla Bergstrm
:available: false
:return_date: '2016-05-25'
- :item:
:title: Osynligt med Alfons
:author: Gunilla Bergstrm
:available: true
:return_date:
- :item:
:title: Pippi Lngstrump
:author: Astrid Lindgren
:available: true
:return_date:
- :item:
:title: Pippi Lngstrump gr ombord
:author: Astrid Lindgren
:available: true
:return_date:
46
Important topics
Searching in a Hash
Let's say that you have the above collection of books or whatever. Now you want to search for a
specific item. If we would like to search for "Pippi Lngstrump", we could do something like this
2.2.3 :002 > collection.detect { |obj| obj[:item][:title] == "Pippi
Lngstrump" }
=> {:item=>{:title=>"Pippi Lngstrump", :author=>"Astrid Lindgren"}
, :available=>true, :return_date=>nil}
So collection is of class: Hash. We are calling #detect on it. What does the #detect method
do?
Note that this will return an exact match, so it requires that you know the exact value of the key you
47
Important topics
48
Javascript Introduction
Javascript Introduction
Javascript Introduction
This week you'll learn a completely new programming language: Javascript. While many developers
use Javascript for all three layers of an application (data/persistence, logic, and presentation), in this
camp we'll mostly be using Ruby for logic and data and often both Ruby and Javascript elements for
presentation. During this week, we will use Javascript for both logic and presentation.
Learning Objectives
Practice pair programming & collaboration using Git and GitHub
Basic understanding of JavaScript
Introduction to JQuery and DOM manipulation
Jasmine testing framework & comparison to RSpec
Observe differences and similarities between Ruby and Javascript
Understand the difference between running code on the server vs. in the browser
Javascript and Ruby (and most programming languages) have a lot in common. They both have
variables, functions, arrays, and hashes. And thousands of other matching structures. But in many
cases these are used slightly differently amongst languages. For instance, in Ruby a variable is
declared like so:
age = 32
In Javascript:
var age = 32;
Functions look very different. In Ruby:
def foo(a, b)
a + b
end
In Javascript we'll typically want to store a function inside a variable:
var fn = function foo(a,b){
return a + b;
}
The following pages will give you a huge list of Javascript variables to play with. Open up any
browser (we suggest Chrome), click "Inspect" and click "Console". Then you can put in any
Javascript code - just like in IRB in the terminal.
Javascript Introduction
Javascript is very unforgiving. In Ruby you can skip spaces, parentheses, etc. If you forget an
end it will probably tell you expected tEND. Javascript isn't like that. You have to be very
careful about your commas, colons, parentheses, curly brackets, and everything else. Every line
in Javascript should end with a ;.
50
Numbers
num = 19;
// note lack of ""
num = 0xfe + 2.343 + 2.5e3;
// hex, floats, exponents
Objects
var newObject = new Object();
// constructor
newObject = {};
// constructor shorthand
newObject.name = "Thomas"
// dynamic attributes
newObject.name = null
// it's there (null item)
delete newObject.name
// it's gone (undefined)
newObject["real age"] = 33;
// array notation/hash table
var person = {
name: "Thomas",
details: {
51
age: 44,
"favorite color": "orange"
}
}
// create object using JSON (Javascript Object Notation)
// note the difference between age: and "favorite color" - one i
s a symbol and one is a string. They are accessed differently.
person.name
// "Thomas"
person.details["favorite color"]
// "orange"
person.details.age
// 44
Arrays
var newArray = [];
// empty array
newArray[3] = "hello";
// grows dynamically
newArray[2] = 13;
// add any datatype
newArray.push(newObject);
// add new item at last index
newArray.pop();
// remove it from last index
52
Datatypes
typeof("text") == "string"
// true
typeof(3) == typeof(3.4) && typeof(0x34) == "number"
// true
typeof(myArray) == "object"
// true (arrays are objects)
typeof(true) == "boolean"
// true
typeof(Math.sin) == "function"
// true
typeof(notThere) == "undefined"
// true (can be useful)
Comparisons
123 == "123"
// true (converts type)
123 === "123"
// false (checks type)
typeof(x) == "undefined"
// true (x isn't there)
x == null
// x is not defined
Numbers
parseInt("123")
// base 10 => 123
parseInt("123", 16);
// base 16 => 291
parseFloat("123.43");
// 123.43
53
isNaN(0/0) == true
// illegal number
3/0 == Infinity
// true (Infinity is displayed when a number exceeds the upper l
imit of the floating point numbers, which is 1.797693134862315E+308)
-3/0 == -Infinity
// true (-Infinity is displayed when a number exceeds the lower
limit of the floating point numbers, which is -1.797693134862316E+30
8)
isFinite(3/0) == true
// false (The isFinite() function determines whether a number is
a finite, legal number. This function returns false if the value is
+infinity, -infinity, or NaN (Not-a-Number), otherwise it returns t
rue.)
alert("Hi John.")
break
default: alert("Who are you?")
}
// switch statement
while (i <= n){
console.log(i);
i++;
}
// do something until a value (n) is reached
// don't forget to have i++ or you will loop forever
for (var i=0; i<=n; i++){
console.log(i);
}
// another way to loop an n number of times
for (var key in person){
console.log(key)
}
// do something with person[key]
55
56
Defining Functions
Defining Functions
Defining Functions
function foo(a,b){
return a + b;
}
// global function
var fn = function(a,b){
return foo(a,b);
}
// save function as a variable
person.fn = function(a,b){
return a + b;
}
// or as part of object
function bar(a,b){
var n = a;
// local var
function helper(x) {
// defining a function inside of another function
return 1/Math.sqrt(x + n);
// can use local vars
}
return helper(b);
// avoid need for global function
}
foo(1,2) == fn(1,2)
// true (3)
bar(1,3);
// 0.5
Javascript Exercises #2
1. Write a function that returns your first name. Call it.
2. Write a new function that takes your name as an input. The function should return your first
name, plus your last name, as one string. (Hint: strings can be combined with a +)
57
Javascript "Classes"
Javascript doesnt have formal class notation, but you can create a constructor and add methods to
it. Examples from here.
function Person(first, last) {
// create "constructor"
this.first = first;
// public variables -- reference current object
this.last = last;
var privateFn = function(first, last){
// private function
}
this.setName = function(first, last){
// public function
this.first = first;
this.last = last;
}
}
Person.prototype.fullName = function() {
// extend prototype
return this.first + ' ' + this.last;
// even at runtime!
}
var bob = new Person("Thomas", "Ochman");
// "new" creates an object
bob.fullName();
// "Thomas Ochman"
Javascript Exercise #3
1. What is this? Does it have an equivalent in Ruby?
58
59
Miscellaneous
Miscellaneous
Miscellaneous
eval("x = 3"); // execute arbitrary code
timer = setTimeout("myfunction()", 1000)
// execute in 1 second (1000ms)
clearTimeout(timer);
// cancel event
60
BMI
Weight Status
62
63
Jasmine - Set up
Jasmine - Set up
Testing with Jasmine
Jasmine is a Behaviour Driven Development framework for testing JavaScript code.
Download Jasmine runner
Create a project folder for the BMI Challenge and extract the content of the jasminestandalone.zip package to that folder.
The package contains some examples of tests. We will not use those tests but we can have a quick
look on how testing with Jasmine is set up by browsing the examples.
From your terminal, open the SpecRunner.html file:
$ open SpecRunner.html
That will open the test runner in your default browser and you should see something like this.
Now, let's have a look at the file structure. Open the content of your project folder in your text editor.
$ atom .
You should see the basic structure of the example project.
64
Jasmine - Set up
You can leave the files as they are if you want, or you can delete them if you want - we will be
deleting them at some point down the road anyway.
The important thing at this moment is to understand how the SpecRunner.html works. In order to
be able to run the tests and have access to the source code, these files needs to be included in the
runner file. Open it up and locate the following section.
<!-- include source files here... -->
<script src="src/Player.js"></script>
<script src="src/Song.js"></script>
<!-- include spec files here... -->
<script src="spec/SpecHelper.js"></script>
<script src="spec/PlayerSpec.js"></script>
As you see, the runner needs to have both the source and the spec files included. Now you can safely
remove those lines or comment them out.
Remember that whatever files you create (both specs and source files) they MUST be included
in the SpecRunner.html!
At this point you are set up with the very basic configuration of Jasmine that will allow you to run
JavaScript tests.
65
First tests
First tests
First tests
Create a new file in your spec folder. Call it person_spec.js.
$ touch spec/person_spec.js
Note that I use the snake case format for my file names. You are free to use any format you
want but be consistent.
We are going to write two test for our Person object. In order to calculate the BMI, we will be needing
a Person to have two attributes, weight and hight.
# spec/person_spec.js
describe("Person", function() {
var person;
beforeEach(function() {
person = new Person({weight: 90, height: 186});
});
it("should have weight of 90", function() {
expect(person.weight).toEqual(90);
});
it("should have height of 186", function() {
expect(person.height).toEqual(186);
});
});
If you reload your SpecRunner.html you'll get two errors.
66
First tests
67
First tests
Okay, the next thing I want you to do is to right-click anywhere on the browser window and choose
Inspect from the pop-up menu. That opens the developer console. Try creating a new instance of a
Person by following the example below.
68
First tests
Cool, the browsers console can function as a way to manually tests your units and functions similarly to ruby's irb.
Let's write some more tests.
# spec/person_spec.js
it("should calculate BMI value", function() {
person.calculate_bmi();
expect(person.bmiValue).toEqual(26.01)
});
it("should have a BMI Message", function() {
person.calculate_bmi();
expect(person.bmiMessage).toEqual("Overweight")
});
In those tests, we are calling a calculate_bmi() function on the person object we have
created. This will fail since we have no such function defined.
We extend the Person function by using prototype, similar to class methods in Ruby.
69
First tests
# src/person.js
Person.prototype.calculate_bmi = function() {
this.bmiValue = 26.01;
this.bmiMessage = "Overweight"
};
That implementation will make the test pass, but it is not what we want, is it? We have just passed in
some hard coded values into that function.
Let's instead call the BMICalculator to do the calculation for us by changing the code to:
# src/person.js
Person.prototype.calculate_bmi = function() {
calculator = new BMICalculator();
calculator.metric_bmi(this);
};
Here we are instantiating a new BMICalculator object and calling a function we call
metric_bmi on it. We are also passing in the current instance of Person to that function. At this
point, we need to shift our attention to the other spec file we need and test the behaviour of the
BMICalculator.
70
The calculator
The calculator
The calculator
Create a spec file for the BMICalculator and call it bmi_calculator_spec.js
Add the following code to it
# spec/bmi_calculator_spec.js
describe("BMICalculator", function() {
var bmi_calculator;
var person;
beforeEach(function() {
person = new Person({weight: 90, height: 186});
calculator = new BMICalculator();
});
it("calculates BMI for a person using metric method", function() {
calculator.metric_bmi(person);
expect(person.bmiValue).toEqual(26.01);
});
});
Make sure that you run your spec runner after each addition and read the error messages.
Add the following code to the src/bmi_calculator.js file
# src/bmi_calculator.js
function BMICalculator(){
};
BMICalculator.prototype.metric_bmi = function(obj) {
var weight = obj.weight;
var height = obj.height;
if (weight > 0 && height > 0) {
var finalBmi = weight / (height / 100 * height / 100);
obj.bmiValue = parseFloat(finalBmi.toFixed(2));
}
};
Why do we need to condition the finalBMI? (Why do we need an if statement?)
Run your tests again.
We still have not set the bmiMessage on the Person. In order to do that, we need to create a
function that sets that message depending on what bmiValue the person has.
71
The calculator
72
With the object model, JavaScript gets all the power it needs to create dynamic HTML:
JavaScript can:
change all the HTML elements in the page
change all the HTML attributes in the page
change all the CSS styles in the page
remove existing HTML elements and attributes
add new HTML elements and attributes
react to all existing HTML events in the page
create new HTML events in the page
Enabling JavaScript
Include javascript inside HTML:
73
<script>
x = 3;
</script>
Reference external file: <script src="http://example.com/script.js"></script>
Not all browsers are JavaScript enabled. You might want to be prepared for that and redirect the user
to a different page if JavaScript is disabled.
<noscript>
<meta http-equiv="refresh" content="0"; URL="http://example.com/
noscript.html"/>
</noscript>
Javascript Exercises #4
1. Open up a website (yes, any website). It might be easier to understand a very simple website perhaps the one you created in the prep course. Select elements in the page by right-clicking on
them and selecting "inspect".
2. Find a div. Give it a border. Change its background color. You can change any of its properties font colors, sizes, and weights, colors, etc.
3. How would you hide an element? Show it again?
74
Web interface
Web interface
Web interface
The next step is to create a web page that will allow for both input of data (create an instance of
Person) and display the calculated results.
For that, we will be using HTML and jQuery. We will also make sure that we have automated tests for
out interface so that we can be sure that everything behaves as we want it to behave.
Testing jQuery
First, we need to make some adjustments to the Jasmine framework by extending Jasmine with
Jasmine-jQuery: a set of jQuery helpers for Jasmine tests.
Download/Copy Jasmine-jQuery from this location and save to lib/jasmine-jquery.js
In your SpecRunner.html locate where you include the Jasmine boot.js. Right underneath,
make sure to include both jasmine-jquery and jquery (we will get that from a remote location
rather than including it in the file structure).
# SpecRunner.html
<script src='https://code.jquery.com/jquery-2.1.4.js'></script>
<!--script src="lib/jquery.min.js"></script-->
<script>
$.holdReady(true);
</script>
<script src="lib/jasmine-jquery.js"></script>
Let's do a manual test to see if you have set everything up and jQuery has been loaded. Reload your
SpecRunner.html, open up the console and type in:
typeof jQuery
If the response you get is "function" then you are all good and jQuery has been loaded. If you get
"undefined" then something is wrong and you need to check your settings.
In regard to the jQuery snippet $ holdReady(true); that we included in the
SpecRunner.html, please see this issue on GitHub for an explanation.
Crete a new test file in the spec folder and call it bmi_ui_spec.js. Add the following code to
that file.
# spec/bmi_ui_spec.js
describe('BMI_UI - index.html', function() {
beforeEach(function() {
jasmine.getFixtures().fixturesPath = '.';
loadFixtures('index.html');
$.holdReady(false);
75
Web interface
});
});
Create the index.html file in the main project folder and add an HTML form that we need to input
the data we need to create a Person and calculate the BMI. We also need to include jQuery (as we
did in the SpecRunner.html) and make sure that the source files for Person and
BMICalculator are loaded.
Go over the code below and make sure that you understand what is going on before you
implement it.
# index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
<script src="src/person.js"></script>
<script src="src/bmi_calculator.js"></script>
<script src='https://code.jquery.com/jquery-2.1.4.js'></script>
</head>
<body>
<form>
<input type="text" id="weight" placeholder="Weight">
<input type="text" id="height" placeholder="Height">
<input type="button" id="calculate" value="Calculate">
</form>
<div>
<span id="display_value"></span>
<span id="display_message"></span>
</div>
</body>
</html>
To open the web page go to your terminal and use the open command to fire up the page in the
browser.
$ open index.html
The page is still very static and nothing happens if you fill in values in the form and click
Calculate. We need to add some jQuery code in order to tell the interface what to do. There are
two ways to do that. We can either create a new js file and include it in our code, or we can insert the
script directly in the HTML. Let's do the latter. In order for HTML to understand that it is dealing
with JavaScript, we need to wrap it in <script></script> tags.
Have a look at the code below and once you understand what it does, add it to the index.html file.
# index.html
<script type="text/javascript">
$(document).ready(function () {
76
Web interface
$('#calculate').click(function () {
var w = parseFloat($('#weight').val());
var h = parseFloat($('#height').val());
var person = new Person({weight: w, height: h});
person.calculate_bmi();
$('#display_value').html('Your BMI is ' + person.bmiValue);
$('#display_message').html('and you are '+ person.bmiMessage);
});
});
</script>
77
Acceptance tests
Acceptance tests
Acceptance tests
In order to be able to load the index.html as a fixture in Jasmine, we need to have a local web
server up and running locally. If we don't we simply can not run tests on the web interface due to
CORS conflicts.
In your main project folder, create a file called Gemfile (note that there is no suffix and that the file
name starts with a capital 'G') and add the following lines.
# Gemfile
gem 'rack'
gem 'sinatra'
Also, create two other files in the main project folder:
config.ru <- Note the suffix!
server.rb
Add the following code to each of the files:
# config.ru
require './server'
run Sinatra::Application
# server.rb
require 'sinatra'
set :public_folder, proc { File.join(root) }
Now, head over to your terminal and start the local web server.
$ rackup
In your browser, type in this URL to access the web page you recently created:
http://localhost:9292/index.html
78
Acceptance tests
You can go ahead and enter some values in the fields and see if everything is working as it should.
To stop the server in your terminal, just press the ctrl + C keys on your keyboard. To start it
again, just type rackup and press enter.
At this point, we are ready to add some tests to our bmi_ui_spec.js.
What we want Jasmine to do is to:
1. Fill in the fields for Weight and Height with values.
2. Click the Calculate button.
3. Assert that the right content is displayed on the page.
Modify your bmi_ui_spec.js with the following code
# spec/bmi_ui_spec.js
describe('BMI_UI - index.html', function() {
beforeEach(function() {
jasmine.getFixtures().fixturesPath = '.';
loadFixtures('index.html');
$.holdReady(false);
$('#weight').val('90');
$('#height').val('186');
$('#calculate').trigger('click');
});
it("displays BMI Value", function() {
79
Acceptance tests
80
Moving on
Moving on
Moving on
At this point, we will shift gears and allow you to work for yourself. There is plenty of functionality
left to implement.
1. Add a function to calculate BMI using the imperial method.
2. Modify the UI to allow the user to choose between the Metric- and Imperial methods
3. Add some CSS to the web interface to create a better look and feel of the application - why not
use some of the great frameworks available out there, like ZURB Foundation or Twitter
Bootstrap?
4. Create a local repository and push up your code to GitHub
5. Deploy your own version of the calculator with Github Pages.
If you want to find some inspiration you can have a look at a slightly styled version of the BMI
Calculator that we have deployed using gh-pages to github.io.
http://craftacademy.github.io/bmi/index.html
81
Moving on
You can make any Github repository into a web page. On your repository, click Settings, then scroll
down to Github Pages and select a branch (the one with the latest changes is best, probably master).
Click "save", then Github will show you the link to your new website. That's it!
82
Instructions
Read this entire README carefully and follow all instructions.
Challenge time: this weekend, until Monday 9am
Feel free to use Google, Stack Overflow, your notes, previously written code, books, etc. but
work on your own
If you refer to or have in whole or partially used the solution of another coach or student, please
put a link to that in your README
If you have a partial solution, push up your code and check in a partial solution under the
assignment submission section
You must submit your assignment submission by 9.30am Monday morning - before the
stand-up.
Learning objective
To write Unit tests with Jasmine
To write Acceptance tests with Jasmine-jQuery
To add HTML elements and get user data
To modify the DOM using jQuery
To deploy your code using GH-Pages
For the web interface there is this User story
As an individual
So that I can check a number when playing FizzBuzz
I would like to be able to enter a number in an field, click a butto
83
n and see
the output according to the FizzBuzz rules.
Tasks
TODO:
Have a look at the karma-jquery-jasmine_boilerplate and follow the set up instructions.
Use the code in the Karma-jQuery-Jasmine-Boilerplate but set the project and repository
name to fizz_buzz_js.
Write your specs and implementation for the Fizz Buzz game as you did in the initial Ruby
challenge - but in JavaScript
Be smart about using Git: commit and push often. Use feature branches.
Submit your work in using the assignment section in this chapter.
Review and reflect
Compare this implementation to the Ruby version we wrote a while back during the prep-course.
1. What are the similarities/differences between the Ruby and js versions?
2. How do you find unit testing with Jasmine in comparison to RSpec?
3. How can you "gamify" Fizz Buzz? Consider the User Interface/Experience. Can the game be
played by multiple players? What about mobile devices?
84
NodeJS
NodeJS
npm & NodeJS
npm, short for Node Package Manager, is two things: first and foremost, it is an online repository for
the publishing of open-source Node.js projects; second, it is a command-line utility for interacting
with said repository that aids in package installation, version management, and dependency
management.
npm is pre-installed with Node. We will not be covering NodeJS in this chapter or using it in this
course. It is a popular Javascript framework for back-end development. We are just using it here to
access some of its tools.
If you have Node, you already have npm! If not, install Node - it's as easy as downloading and then
running an installer.
Visit the NodeJS website and download the installer.
85
NodeJS
Once your download is completed, run the installer and follow that instructions.
86
NodeJS
You can check your version by going to your terminal and typing in this command
$ node -v
87
NodeJS
Now you are ready to start using npm. You can find more documentation about packages on the NPM
website.
88
Checkout challenge
Checkout challenge
Checkout Challenge
Our client is an on-line marketplace, here is a sample of some of the products available on our site:
Product code Name Price
001
002
003
Tie
$9.25
Sweater $45.00
Skirt
$19.95
Our marketing team want to offer promotions as an incentive for our customers to purchase these
items.
If you spend over $60, then you get 10% off of your purchase.
If you buy 2 or more ties then the price drops to $8.50.
Our check-out can scan items in any order, and because our promotions will change, it needs to be
flexible regarding our promotional rules.
The interface to our checkout looks like this:
co = Checkout.new(promotional_rules)
co.scan(item)
co.scan(item)
price = co.total
Implement a checkout system that fulfills these requirements.
Test data
--------Basket: 001,002,003
Total price expected: $66.78
Basket: 001,003,001
Total price expected: $36.95
Basket: 001,002,001,003
Total price expected: $73.76
89
Functionality
The functionality we are looking for is pretty simple yet not in any way trivial.
1. When the web page is being accessed, get the users current position using the
navigator.geolocation.getCurrentPosition() method.
2. Get the weather info for the obtained position by querying the OWM API
3. Parse the response
4. Display appropriate information on the web page without reloding the page.
Learning objective
Learn how to get hold of data from an external source.
Learn about JSON objects
Learn about AJAX and DOM manipulation
Learn how to stub network calls in Jasmine
Technology
In order to access the weather service you need to sign up for the FREE tier on their website and get
an API key.
90
"deg":45,
"gust":7.2
},
"clouds":{
"all":36
},
"dt":1471467557,
"sys":{
"type":3,
"id":9444,
"message":0.01,
"country":"SE",
"sunrise":1471405395,
"sunset":1471459624
},
"id":2689287,
"name":"Nordstaden",
"cod":200
}
Extended functionality
If you are finished with the above implementation and it is fully tested, you can go on and implement
the following:
1. Add a map display to the page
2. Add a possibility to manually set the location and update the results.
One good way of setting the location manually is to use the Google Places API Web Service. There
are libraries you can use to activate autocomplete on an input field in your UI. You might want to
trigger an event on selecting a location in the pulldown and update the map AND query the OWM
API.
92
learn about deployment to Heroku and the benefits of services like Heroku, DigitalOcean and
AWS
Tools
During this project you will be using a variety of tools. You have used some of them in other
challenges, but many will be new to you.
Sinatra as web framework
ZURB Foundation 6 as CSS framework
PostgreSQL as a database with DataMapper as ORM
Warden for user authentication
Cucumber for Acceptance tests
RSpec for Unit tests
Pony for creating end editing Entity-Relationship diagrams
GitHub to store your code and make it available for the entire team
Waffle.io or ZenHub.io as Project Management tool for tracking features, issues, bugs, etc.
The purpose of this exercise is to simulate a real project and prepare you for the mid course
project that you will be working on in week 6 and/or 7 of the Bootcamp.
Scope
The first version of the application has limited functionality
The owner need to access a protected part of the application to set up information about his
Restaurant and his Menu
A Menu needs to consist of many Dishes
Each Dish has a Category. It is either a Starter, a Main course or an Dessert
Visitors of the site can add Dishes to an Order
In order to finalize an Order (Check out) a Visitor needs to become a Registered User
Order need to calculate a Total price and a Pick up time (30 minutes)
Nothing else should be considered or implemented.
94
Agile principles
There are different Agile methodologies you can use in your projects and we will be talking a lot
about that and introducing different agile techniques as we move along in the course. For now, you
should always follow these core Agile principles:
focus on user needs
deliver iteratively
keep improving how your team works
fail fast and learn quickly
keep planning
Travis: I should get one of those signs that says "One of these days I'm gonna get organezized".
Betsy: You mean organized?
Travis: Organezized. Organezized. It's a joke. O-R-G-A-N-E-Z-I-Z-E-D...
Betsy: Oh, you mean organezized. Like those little signs they have in offices that says,
95
"Thimk"?
Taxi Driver (1976)
Your team should plan together, review these plans regularly and change them based on your progress
and any new facts and requirements that arise during the development process.
Both Waffle.io and ZenHub.io are project management tools powered by your GitHub Issues & Pull
Requests. Waffle.io is the tool of choice for many AgileVentures projects and provides a web based
interface for managing Issues and track Pull Requests. ZenHub provides similar functionality (and
more) as a browser extension. It is up to you to decide what tool your team want to use.
Structure
We will make use of 4 columns in Waffle (or ZenHub if you choose to use that tool) to track our
work.
96
Backlog
Any issue that we see as important for the near future lives in the Backlog. Issues in this column are
generally prioritized from top to bottom with the top items having the highest priority.
Ready
Any issue that we would like to work on soon is moved to Ready. As we move ahead, we pull issues
from Backlog into Ready, so we have an idea of the most important items to work on for at the
moment.
In Progress
Any issue that is currently being worked on is put into the In Progress column and assigned to a
developer. By limiting the number of issues you work on at one time helps you focus on what needs
to be done.
Done
Any issues that have been closed are reflected in the Done column. For pull requests, this means that
the code has been merged and deployed.
Stories
We will use 3 types of stories in our project plan: Features, Chores and Bugs
Features
Features are stories that provide verifiable business value to the team's customer. Examples of
97
features include "add a 'special instructions' field to the checkout page", "purchase history should load
in half a second", and "add a new method addToInventory to the public API". Features are worth
points and therefore must be estimated.
Chores
Chores are stories that are necessary, but provide no direct, obvious value to the customer. Examples
include "sign up for access to geocoding service" and "Find out why the detailed test suite takes so
long". Chores can represent "code debt", and/or points of dependency on other teams.
Bugs
Bugs represent unintended behavior that can be related to features, for example "login box is wrong
color", and "price should be non-negative".
Introduction to Waffle.io: https://youtu.be/yEbRaA3rYuA
Introduction to ZenHub.io: https://youtu.be/TRu7vKCg920
98
Lo-Fi's
Low-fidelity prototypes (LoFi's) of the systems views are rough representations of concepts that help
us validate those concepts early on in the design process. Working with transforming user stories to
low-fidelity prototypes is a unique way to radically improve your work and to build a UX in which
user's needs can be truly realized.
Design thinking advocates for "thinking with your hands" as a way to build empathetic
solutions.
Lean startup relies on early validation and the development of a minimum viable product to
iterate on.
User-centered design calls for a collaborative design process where users deliver continual
feedback based on their reactions to a product's prototype.
100
Task
I want you to draw an ERD of the SlowFood system. You can use this ERD Tool:
https://editor.ponyorm.com/
101
102
103
That will open the IRB console with all your gems and dependencies loaded. Now you can try to
create a new user using the class definition in the legacy code.
2.2.3 :001 > User.count
=> 1
2.2.3 :002 > my_user = User.new
=> # \<User @id=nil @username=nil @password=nil>
2.2.3 :003 > my_user.username = "Tochman"
=> "Tochman"
2.2.3 :004 > my_user.password = "my_secret_password"
=> "my_secret_password"
2.2.3 :005 > my_user.save
=> true
2.2.3 :006 > my_user
=> # \<User @id=2 @username="Tochman" @password="$2a$10$Lz6uIslKglm
zGiiGyjjfh.EfGA1Bp2h4PCjo7W0A2M4s/PIJSVOb6">
2.2.3 :007 >
describe User do
it { is_expected.to have_property :id }
it { is_expected.to have_property :username }
it { is_expected.to have_property :password }
it { is_expected.to have_one :restaurant }
end
And a spec for the Restaurant class
!FILENAME spec/restaurant_spec.rb
require './lib/models/restaurant'
describe Restaurand do
it { is_expected.to have_property :id }
it { is_expected.to have_property :name }
it { is_expected.to belong_to :user }
end
Your job will be to make those tests to pass. Use the DataMapper documentation to find out how to
set up relationships between entities/classes.
106
108
This is perfectly normal. What Cucumber is doing is helping you in defining so called step
definitions, that are the actual implementation of your tests so that they can be understood by ruby.
Confusing? Let me get you started by implementing the steps above.
Modify the fetures/support/env.rb file to make cucumber actually start your application
and load the necessary dependencies.
!FILENAME fetures/support/env.rb
ENV['RACK_ENV'] = 'test'
require File.join(File.dirname(__FILE__), '..', '..', 'lib/controlle
r.rb')
require 'capybara'
require 'capybara/cucumber'
require 'rspec'
require 'pry'
Capybara.app = SlowFood
class SlowFoodWorld
include Capybara::DSL
include RSpec::Expectations
include RSpec::Matchers
end
World do
SlowFoodWorld.new
109
end
Create a new file in the features/step_definitions folder.
$ touch features/step_definitions/basic_steps.rb
Add the following code to that file
!FILENAME features/step_definitions/basic_steps.rb
You can implement step definitions for undefined steps with these sn
ippets:
Given(/^I am on the "([^"]*)"$/) do |arg1|
pending # Write code here that turns the phrase above into concret
e actions
end
Given(/^I click on the "([^"]*)" link$/) do |arg1|
pending # Write code here that turns the phrase above into concret
e actions
end
Then(/^I should be on the registration page$/) do |arg1|
pending # Write code here that turns the phrase above into concret
e actions
end
At the moment all the above step definitions are empty, so we need to add some commands for each
of the steps. I will introduce some Capybara commands to the Given steps and an assertion to the
Then step.
!FILENAME features/step_definitions/basic_steps.rb
Given(/^I am on the "([^"]*)"$/) do |page|
visit '/'
end
Given(/^I click on the "([^"]*)" link$/) do |link|
click_link_or_button link
end
Then(/^I should be on the registration page$/) do
expect(current_path).to eq '/auth/login'
end
If you save your work and run cucumber again you should see something like this.
110
check('A Checkbox')
uncheck('A Checkbox')
attach_file('Image', '/path/to/image.jpg')
select('Option', from: 'Select Box')
=scoping=
within("//li[@id='employee']") do
fill_in 'Name', with: 'Thomas'
end
within(:css, "li#employee") do
fill_in 'Name', with: 'Thomas'
end
within_fieldset('Employee') do
fill_in 'Name', with: 'Thomas'
end
within_table('Employee') do
fill_in 'Name', with: 'Thomas'
end
=Querying=
expect(page).to have_xpath('//table/tr')
expect(page).to have_css('table tr.foo')
expect(page).to have_content('foo')
expect(page).not_to have_content('foo')
find_field('First Name').value
find_link('Hello').visible?
find_button('Send').click
find('//table/tr').click
locate("//*[@id='overlay'").find("//h1").click
all('a').each { |a| a[:href] }
=Scripting=
result = page.evaluate_script('4 + 4');
=Debugging=
save_and_open_page
112
gem 'capybara-webkit'
gem 'dm-rspec'
end
Don't forget to do bundle install once you have made all the changes.
In terminal run the following command:
$ rspec --init
You should see the following output:
create .rspec
create spec/spec_helper.rb
Now, open .rspec file (it is located in your main project folder but it is a hidden file, so your text
editor might not show it) and modify it so that the first line is set to:
!FILENAME .rspec
--format documentation
...
Now, in your terminal, type in rspec and hit enter.
The output you see should be something like:
No examples found.
Finished in 0.00023 seconds (files took 0.5029 seconds to load)
0 examples, 0 failures
We are not quite done yet. Sorry. :-(
I want you to open the spec/spec_helper.rb file. You'll see a lot of lines. Most of them are not
needed so I would like you to delete all lines that begin with the #sign. These are comments and we
don't need them - for now, they are just a distraction.
When you are done you should see something like this:
!FILENAME spec/spec_helper.rb
RSpec.configure do |config|
config.expect_with :rspec do |expectations|
expectations.include_chain_clauses_in_custom_matcher_description
s = true
end
config.mock_with :rspec do |mocks|
mocks.verify_partial_doubles = true
end
end
Not so bad, ey?
114
Alright, we need to add some of the libraries/gems we want to use in our spec_helper. Modify
your file to include the settings below.
!FILENAME spec/spec_helper.rb
ENV['RACK_ENV'] = 'test'
require File.join(File.dirname(__FILE__), '..', 'lib/controller.rb')
require 'capybara'
require 'capybara/rspec'
require 'rspec'
require 'dm-rspec'
...and in the RSpec.configure block, add:
!FILENAME spec/spec_helper.rb
...
config.include Capybara::DSL
config.include DataMapper::Matchers
...
Now, run rspec again and you should receive no errors.
No examples found.
Finished in 0.00032 seconds (files took 0.72891 seconds to load)
0 examples, 0 failures
Okay, this means that you have successfully added and set up RSpec as a testing framework.
Cucumber
Make sure that cucumber is added to your Gemfile's :development, :test group as a
dependency.
!FILENAME Gemfile
group :development, :test do
gem 'cucumber'
end
If it's not, add it and run bundle install. Once that is complete I'd like you to initiate Cucumber
by simply typing in:
$ cucumber --init
That will create some files and folders for you:
create features
create features/step_definitions
create features/support
create features/support/env.rb
Okay, now, in your terminal, you type in:
115
$ cucumber
The output you will see should look something like this:
0 scenarios
0 steps
0m0.000s
Alright, we have installed and initiated the first testing framework. Big step.
116
What is Middleman?
Middleman "is a static site generator using all the shortcuts and tools in modern web
development"
Middleman site
Static websites are widely used because they are fast, easy to setup. They are served to the user
exactly as stored and there is no transaction to a database. It's pretty much HTML, CSS and
JavaScript if needed.
You might be wondering now, why go through the trouble of using a tool like Middleman to build a
static website?
Building a static site with many pages can lead to having lots of repeated code across. Updating them
during development will quickly become a nightmare. Middleman will allow you to structure your
project in a modular way using things like layouts, partials, etc. Giving you a DRY and more
manageable project.
Middleman uses the Ruby programming language. But don't worry if you are still new to Ruby as you
will likely not write too much of it while build your site.
Now let's install middleman and build our first site.
117
Week lab
Week lab
Week lab
The weekly challenge is an individual one. You are supposed to build an individual portfolio site that
will showcase the projects you have been working on. We will leave the visual aspects of the site
itself to your artistic ambitions, but we have some requirements as to the functionality we want the
site to deliver:
1.
2.
3.
4.
5.
6.
7.
8.
9.
Note: You are welcome to work in pairs if you like, but it is not a requirement and if you do, you
need to figure out how to complete your individual sites and still be able to pair program.
Learning Objectives
The Middleman framework
Using ruby to create static websites
HAML
How can HAML improve your workflow
Write less code and avoid the html mess
Deployment
learn about deployment of static sites.
118
Setup Middleman
Setup Middleman
Setup Middleman
If you don't have Ruby installed yet, please refer to our setup instructions for Linux or Mac OS X
Once you're done, head to your terminal and run the following command:
$ gem install middleman
This will install Middleman, its dependencies and command line tools for using Middleman.
119
Setup Middleman
Great! We have successfully setup middleman and created our project. Now let's go through the files
that have been generated for us.
Directory Structure
A tipical middleman application will have a directory structure that looks like this:
my_site/
+-- .gitignore
+-- Gemfile
+-- Gemfile.lock
+-- config.rb
+-- source
+-- images
+-- background.png
+-- middleman.png
+-- index.html.erb
+-- javascripts
+-- all.js
+-- layouts
+-- layout.erb
+-- stylesheets
+-- all.css
+-- normalize.css
120
Setup Middleman
This is where your static website files will be compiled and exported to.
The lib directory
The lib directory will house your external Ruby modules. These modules will contain helpers to use
while building your application.
The data directory
Data files allow you to create .yml, .yaml or .json files in the data folder and makes this
information available in your templates. We will be covering Data files in another section of this
chapter.
121
Haml:
%ul.someClass
%li#someID
As you can see, the "." and "#" symbols are used to denote classes and IDs respectively. This is great
because you're already familiar with this method in CSS. That familiarity means that once you really
dive in, you'll be able to pick up Haml in no time. Here's how this code compiles.
HTML:
<ul class='someClass'>
<li id='someID'></li>
</ul>
HTML5
Just in case you're wondering, Haml works just fine with all of the cool new tags inside of HTML5. In
fact, it doesn't really care what type of tags you try to create. If you type %somecrazytag the output
result will be regardless of the fact that this isn't even valid HTML.
Here's a quick example to illustrate that uses the new header, nav, section and footer HTML5 tags.
Haml:
.wrapper
%header
%nav
%ul
%li one
%li two
%li three
124
%section
.info
%p Lorem ipsum doller set
%section
.info
%p Lorem ipsum doller set
%section
.info
%p Lorem ipsum doller set
%footer
%p Lorem ipsum doller set
HTML:
<div class='wrapper'>
<header>
<nav>
<ul>
<li>one</li>
<li>two</li>
<li>three</li>
</ul>
</nav>
</header>
<section>
<div class='info'></div>
<p>Lorem ipsum doller set</p>
</section>
<section>
<div class='info'></div>
<p>Lorem ipsum doller set</p>
</section>
<section>
<div class='info'></div>
<p>Lorem ipsum doller set</p>
</section>
<footer>
<p>Lorem ipsum doller set</p>
</footer>
</div>
Ruby Injections
HAML's approach is to reduce <%= %> to just =. The HAML engine assumes that if the content
starts with an =, that the entire rest of the line is Ruby. For example, the flash paragraph above would
be rewritten like this:
%p= flash[:notice]
Note that the = must be placed against the %p tag.
Ruby commands
We've seen plain text, HTML elements, and printing Ruby. Now let's focus on non-printing Ruby.
One of the most common uses of non-printing Ruby in a view template is iterating through a
collection. In ERB we might have:
<ul id='articles'>
<% @articles.each do |article| %>
<li><%= article.title %></li>
<% end %>
</ul>
The second and fourth lines are non-printing because they omit the equals sign. HAML's done away
with the <%. So you might be tempted to write this:
%ul#articles
@articles.each do |article|
%li= article.title
Content with no marker is interpreted as plain text, so the @articles line will be output as plain
text and the third line would cause a parse error.
Conclusion
HAML templates are cleaner, easier to read, easier to maintain, and easier to develop on.
An anonymous developers opinion will close this section:
HAML makes me faster. A lot faster. Not having to track down where a closing tag was omitted,
never accidentally crossing tag scope, and being able to trivially ascertain the selector path of a
given bit of markup makes templating fast. I do a lot of it "the old way" too, and...it's just not as
fun. It's frustrating, and slow, and it's not because I'm bad at it - I did it for years before HAML it's because if you take the time to learn it, HAML is genuinely better.
Happy HAML coding!
127
128
SASS
SASS
SASS
[TODO: Rewrite this section]
<!-- Include jQuery -->
= javascript_include_tag "//ajax.googleapis.com/ajax/libs/jquery/1.1
1.0/jquery.min.js"
<!-- Include Foundation -->
= stylesheet_link_tag "//cdn.jsdelivr.net/foundation/5.5.0/css/found
ation.min.css" %>
= javascript_include_tag "//cdn.jsdelivr.net/foundation/5.5.0/js/fou
ndation.min.js"
129
Accessing data
Accessing data
Accessing data
Middleman allows you to create .yml, .yaml or .json files in a folder called data and makes
this information available in your templates. The data folder should be placed in the root of your
project i.e. in the same folder as your project's source folder.
Let's add a simple test for listing some project information on your index page.
!FILENAME spec/features/index_spec.rb
it 'displays project list' do
expect(page).to have_css '.projects'
within '.projects' do
expect(page).to have_content 'My First Website'
expect(page).to have_content 'FizzBuzz'
end
end
In order to make this test go green, we need to got through some steps:
1.
2.
3.
4.
!FILENAME data/projects.yml
- name: My First Website
description: Simple HTML5 site I've build during the PrepCourse
- name: FizzBuzz
description: My first Test driven ruby project
!FILENAME source/index.html.haml
.projects
- data.projects.each do |project|
%h4= project[:name]
%p= project[:description]
Note that Middleman knows how to access the data folder and the projects.yml file as long as
you use the right file name in your code.
Knowing this, you can extract information about your projects from the template and store it in
a YAML file and display it on any page you want.
130
Partials
Partials
Partials
Partials are a way of sharing content across pages and helps you to keep your code DRY. They can be
used both in templates and in layouts.
Let's assume that we want to display a footer on our page.
!FILENAME spec/features/index_spec.rb
it 'renders footer partial' do
expect(page).to have_selector 'footer'
within 'footer' do
expect(page).to have_content 'My Portfolio'
expect(page).to have_content 'Built using the awesome Middleman
framework'
end
end
In order to group our partials we want to create a partials folder in our source folder.
In that folder, let's create a file called _footer.html.haml. Note the underscore at the
beginning of the filename!
!FILENAME source/partials/_footer.html.haml
%footer
%h3 My Portfolio
%p Built using the awesome Middleman framework
Under the = yield command in your main layout file, place the call to render the footer partial.
!FILENAME layouts/layout.html.haml
= partial 'partials/footer'
If you run your specs now, the one we just added should go green.
Knowing this, you can add a more complex partials to your application and keep your code
DRY
131
Partials
Figure: Still not much to show to the workd but with some more content...
132
!FILENAME config.rb
require 'extensions/build_cleaner'
configure :build do
activate :relative_assets
activate :build_cleaner
end
Great now we're all set to deploy the application.
134
Description of Contents
The default directory structure of a generated Ruby on Rails application:
|-- app
| |-- assets
| |-- images
| |-- javascripts
| `-- stylesheets
| |-- controllers
| |-- helpers
| |-- mailers
| |-- models
| `-- views
| `-- layouts
|-- config
| |-- environments
| |-- initializers
| `-- locales
|-- db
|-- doc
|-- lib
| `-- tasks
|-- log
|-- public
|-- script
|-- test (or spec)
| |-- fixtures
135
| |-- functional
| |-- integration
| |-- performance
| `-- unit
|-- tmp
| |-- cache
| |-- pids
| |-- sessions
| `-- sockets
`-- vendor
|-- assets
`-- stylesheets
`-- plugins
app
Holds all the code that's specific to this particular application.
app/assets
Contains subdirectories for images, stylesheets, and JavaScript files.
app/controllers
All controllers should descend from ApplicationController which itself descends from
ActionController::Base.
app/models
Holds models that should be named like post.rb. Models descend from ActiveRecord::Base by
default.
app/views
Holds the template files for the views.
app/views/layouts
Holds the template files for layouts to be used with views. This models the common
header/footer method of wrapping views. In your views, define a layout using the layout
:default and create a file named default.html.erb. Inside default.html.erb,
call <% yield %> to render the view using this layout.
app/helpers
Helpers can be used to wrap functionality for your views into methods.
config
Configuration files for the Rails environment, the routing map, the database, and other
dependencies.
db
Contains the database schema in schema.rb. db/migrate contains all the sequence of Migrations
for your schema.
136
doc
This directory is where your application documentation will be stored when generated using
rake doc:app
lib
Application specific libraries. Basically, any kind of custom code that doesn't belong under
controllers, models, or helpers. This directory is in the load path.
public
The directory available for the web server. Also contains the dispatchers and the default HTML
files. This should be set as the DOCUMENT_ROOT of your web server.
script
Helper scripts for automation and generation.
test or spec
Unit and functional tests along with fixtures. When using the rails generate command,
test files will be generated for you and placed in this folder.
vendor
External libraries that the application depends on. Also includes the plugins subfolder.
137
Testing\/Development Cycle
A good cycle to follow is this outside-in approach:
1.
2.
3.
4.
5.
6.
Write a high-level (outside) business value example (using Cucumber) that goes red
Write a lower-level (inside) RSpec example for the first step of implementation that goes red
Implement the minimum code to pass that lower-level example, see it go green
Write the next lower-level RSpec example pushing towards passing step 1
Repeat steps 3 and 4 until the high-level test (1) goes green
Start over by writing a new high-level test
Once you are done it is time to scaffold the Rails application we will be working with.
$ rails new rails_demo --database=postgresql --skip-test --skip-bund
le
The --database=postgresql selects PostgreSQL as the database (The out-of-the-box
setting is MySQL)
The --skip-test option skips configuring for the default testing tool.
The --skip-bundle option prevents the generator from running bundle install
automatically.
$ cd rails_demo
Before we move on we need to add another configuration to the Rails application to avoid the
generators to scaffold too many files. Make the following modification to the
config/application.rb file:
class Application < Rails::Application
# Disable generation of helpers, javascripts, css, and view, helpe
r, routing and controller specs
config.generators do |generate|
generate.helper false
generate.assets false
generate.view_specs false
generate.helper_specs false
generate.routing_specs false
generate.controller_specs false
end
# ...
end
One last thing in this setup process, open the .rspec file (it is located in your main project folder
but it is a hidden file, so your text editor might not show it) and modify it so that the first line is set to:
--format documentation
Now, in your terminal, type in bundle exec rspec and hit enter.The output you see should be
something like:
$ bundle exec rspec
No examples found.
Finished in 0.00023 seconds (files took 0.5029 seconds to load)
0 examples, 0 failures
Cucumber
Add cucumber-rails and database_cleaner gems to the test group of the Gemfile.
group :development, :test do
[...]
gem 'cucumber-rails', require: false
gem 'database_cleaner'
end
The database_cleaner gem is used to ensure a clean database state for testing and will make
your development easier.
Run this command to bootstrap the application with Cucumber:
$ bundle exec rails generate cucumber:install
This will generate Cucumber configuration files and set up the database for Cucumber tests. The
generator will also create all the code you need to integrate DatabaseCleaner into your Rails project.
As a last step, you will need to create the database and run the migrate command (even if we have
not created any tables and columns yet)
140
pending # Write code here that turns the phrase above into concret
e actions
end
Then(/^I should see "([^"]*)"$/) do |arg1|
pending # Write code here that turns the phrase above into concret
e actions
end
Let's copy those steps into features/step_definitions/landing_page_steps.rb and
edit them:
When(/^I am on the landing page$/) do
visit root_path
end
Then(/^I should see "([^"]*)"$/) do |content|
expect(page).to have_content content
end
If you run the scenario again, you will see that it fails since the route root_path is not defined.
Let's add it to our routes.rb file:
config/routes.rb
Rails.application.routes.draw do
root controller: :landing, action: :index
end
Now implement the controller that will serve the / route. For that, we will use a Rails Generator.
$ rails generate controller landing index
create app/controllers/landing_controller.rb
route get 'landing/index'
invoke erb
create app/views/landing
create app/views/landing/index.html.erb
invoke rspec
Now that we have the root_path setup with the LandingController, let's run our tests again. The
first step of our scenario is now green.
Using the default profile...
[...]
Scenario: Viewing list of articles on application's landing page # f
eatures/list_articles.feature:6
When I am on the landing page # features/step_definitions/landing_
page_steps.rb:1
Then I should see "A breaking news item" # features/step_definitio
ns/landing_page_steps.rb:5
expected to find text "A breaking news item" in "Landing#index F
ind me in app/views/landing/index.html.erb" (RSpec::Expectations::Ex
pectationNotMetError)
142
We now have a Background block that will set the stage for our scenario by creating two articles in
our database. We then check to see that both articles are displayed on our landing page. If we run
cucumber now, we get an undefined step with the following code snippet:
Given(/^the following articles exists$/) do |table|
# table is a Cucumber::MultilineArgument::DataTable
pending # Write code here that turns the phrase above into concret
e actions
end
Let's add that to our step definition.
Given(/^the following articles exists$/) do |table|
table.hashes.each do |hash|
Article.create!(hash)
end
end
Now run the feature file and see a new error message:
...
uninitialized constant Article (NameError)
...
Failing Scenarios:
cucumber features/list_articles.feature:12 # Scenario: Viewing list
of articles on application's landing page
1 scenario (1 failed)
6 steps (1 failed, 5 skipped)
0m0.155s
This is a very crucial step in our development process. This is as far as you can go in the high-level
tests - at this point, we need to shift our attention to the process of creating our units (Article) using
tests as a blueprint. It's time to focus on the lower-level (inside) tests using RSpec.
Let's think about how we want our Articles to be structured:
We need to be able to store Articles in our database - meaning we need to create a Model
We want each article to have a title and content - we need to add these attributes to our
Article model
We don't want to store articles that don't have title and content present - meaning we
need to add a validation that these attributes are not empty
We want to make sure that there is a valid Factory to use in our test environment
The first thing we want to do is to create a spec file in the spec/models folder called
article_spec.rb. Let's add some tests (we will use the matchers provided to us by the
shoulda-matchers gem that extends the built-in RSpec matchers).
spec/models/article_spec.rb
require 'rails_helper'
144
articles.
class LandingController < ApplicationController
def index
@articles = Article.all
end
end
And replace the content of the view that will render LandingController#index action and list
all posts.
<ul>
<% @articles.each do |article| %>
<li>
<%= article.title %><br />
<%= article.content %>
</li>
<% end %>
</ul>
At this point, if you run cucumber you should see all tests passing green.
Wrap up
During this walkthrough, we have completed one Acceptance-Unit Cycle (without the refactoring
part) and added a simple feature that allows visitors to view articles on the landing page of the
application.
Using our tests we were able to craft out some functionality and delivered the objective of the feature:
To list articles on the landing page.
We created a route (URL)
We created an Article model with attributes and validations
We created a controller with an index method that fetches all articles from the database and
stores the collection in a variable that is made available to the view template
We created a view that iterates through a collection of articles
In the next cycle, we would certainly add more scenarios to this feature to test other paths. What
should happen when there are no articles in the system? Will multiple articles be displayed correctly?
Etc..
It takes a little practice but with this approach you are constantly in charge of the workflow and
know what the next step of your implementation should be.
146
Rails Messaging
Rails Messaging
Rails Messaging
The scope
We will be building an application that allows users send and receive messages between themselves.
Our application should keep a list of messages organized into folders: sentbox, inbox and trash
and also ability to send notifications via mail.
Let's start out by creating a new rails application and call it rails_messaging. We will use
PostgreSQL and skip the Test Unit gem (We'll be seting up Cucumber for acceptance testing.
$ rails new rails_messaging --database=postgresql --skip-test-unit
We'll be using the Bootstrap css framework under the hood for some quick styling. Next up, we will
need to set up and authentication mechanism, we'll use Devise for that. Let's also throw in the
Mailboxer gem at this point and leave it to simmer for a while while we work on authentication.
gem 'bootstrap-sass'
gem 'devise'
gem 'mailboxer'
We then install our gems using the bundle command
$ bundle install
Authentication
We now need to run Devise's generator to install an initializer that will describe all options available
to configure Devise.
$ rails generate devise:install
Completing the additional steps as described on the post-install messages that appear after running the
above generator, lets create a controller and name it welcome with one action.
$ rails g controller welcome index
Let's make the root path of our application point to the index action of our newly created welcome
controller
!FILENAME config/routes.rb
Rails.application.routes.draw do
root 'welcome#index'
end
We need to tweak the application.html layout file a bit to add the flash messages
!FILENAME app/views/layoits/application.html.erb
147
Rails Messaging
<!DOCTYPE html>
<html>
<head>
<title>Messenger</title>
<%= stylesheet_link_tag 'application', media: 'all', 'data-turboli
nks-track' => true %>
<%= stylesheet_link_tag "//maxcdn.bootstrapcdn.com/font-awesome/4.
3.0/css/font-awesome.min.css" %>
<%= javascript_include_tag 'application', 'data-turbolinks-track'
=> true %>
<%= csrf_meta_tags %>
</head>
<body>
<%= render 'layouts/nav' %>
<% flash.each do |key, value| %>
<div class="text-center <%= flash_class(key) %>">
<%= value %>
</div>
<% end %>
<div class="container">
<%= yield %>
</div>
</body>
</html>
You'll notice the use of the flash_class function that takes the key as an argument, that's actually
a helper method I define in the application_helper.rb in the helpers folder. Used to return the
appropriate bootstrap classes based on the flash key.
!FILENAME app/helpers/application_helper.rb
module ApplicationHelper
def flash_class(level)
case level.to_sym
when :notice then "alert alert-success"
when :info then "alert alert-info"
when :alert then "alert alert-danger"
when :warning then "alert alert-warning"
end
end
end
`
We'll also add _nav partial that holds our applications navigation in the layouts folder.
!FILENAME views/layouts/_nav.html.erb
<!-- Fixed navbar -->
<nav class="navbar navbar-inverse navbar-fixed-top">
148
Rails Messaging
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-tog
gle="collapse" data-target="#navbar" aria-expanded="false" aria-cont
rols="navbar">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<%= link_to "Messenger", root_path, class: "navbar-brand" %>
</div>
<div id="navbar" class="navbar-collapse collapse">
<ul class="nav navbar-nav pull-right">
<li class="active"><%= link_to "Login", "#" %></li>
<li><%= link_to "Sign up", "#" %></li>
</ul>
</div><!--/.nav-collapse -->
</div>
</nav>
`
We also want to copy over the Devise's views to our project so that we can tweak them
$ rails generate devise:views
Let's create a User model configured with Devise modules, Devise has an awesome generator for
this:
$ rails generate devise User
$ rake db:migrate
The generator also configures your config/routes.rb file to point to the Devise controller. You
can check the User model for any additional configuration options you might want to add but for our
case we will leave it at its defaults. At this point we can update the login and sign up links in our
_nav partial to point to the appropriate routes.
!FILENAME views/layouts/_nav.html.erb
<!-- [.....] -->
<ul class="nav navbar-nav pull-right">
<% if user_signed_in? %>
<li><%= link_to "Hello, #{current_user.name}", "#" %></li>
<li><%= link_to "Logout", destroy_user_session_path, method:
:delete %></li>
<% else %>
<li class="active"><%= link_to "Login", new_user_session_pat
h %></li>
<li><%= link_to "Sign up", new_user_registration_path %></li>
<% end %>
</ul>
<!-- [.....] -->
149
Rails Messaging
We want users to provide their names during registration, lets create a migration to add a name
attribute to our users.
$ rails g migration AddNameToUsers name:string
$ rake db:migrate
We then add the name input field to Devise's views.
!FILENAME app/views/devise/registrations/new.html.erb
<h2>Sign up</h2>
<%= form_for(resource, as: resource_name, url: registration_path(res
ource_name)) do |f| %>
<%= devise_error_messages! %>
<div class="form-group">
<%= f.label :name %>
<%= f.text_field :name, class: "form-control" %>
</div>
<!-- [.....] -->
<%= f.submit "Sign up", class: "btn btn-primary" %>
<% end %>
`
!FILENAME app/views/devise/registrations/edit.html.erb
<h2>Edit <%= resource_name.to_s.humanize %></h2>
<%= form_for(resource, as: resource_name, url: registration_path(res
ource_name), html: { method: :put }) do |f| %>
<%= devise_error_messages! %>
<div class="form-group">
<%= f.label :name %>
<%= f.text_field :name, class: "form-control" %>
</div>
<!-- [....] -->
<%= f.submit "Update", class: "btn btn-primary" %>
<% end %>
<!-- [...] -->
`
Since we are running this app on Rails 4, we will need to whitelist the name parameter. In our
application controller file:
!FILENAME app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
# Prevent CSRF attacks by raising an exception.
# For APIs, you may want to use :null_session instead.
150
Rails Messaging
Mailboxer
We've already added the Mailboxer gem already in our Gemfile. Now we can proceed to install it and
update our database.
$ rails g mailboxer:install
$ rake db:migrate
The generator creates an initializer file mailboxer.rb which you should use this to edit the default
mailboxer settings. The options are quite straight forward, tweak them to your liking. We will then
change the default name_method since we already have the name attribute in our user model to
prevent conflict.
!FILENAME config/initializers/mailboxer.rb
Mailboxer.setup do |config|
# [...]
#Configures the methods needed by mailboxer
config.email_method = :mailboxer_email
config.name_method = :mailboxer_name
# [...]
end
The Model
For us to equip our User model with Mailboxer functionalities we have to add
acts_as_messageable to it. This will enable us send user-to-user messages. We also need to add
an identity defined by a name and an email in our User model as required by Mailboxer.
Our model must therefore have this specific methods. These methods are: mailboxer_email and
mailboxer_name as stated in our mailboxer initializer file. You should make sure to change this
when you edit their names in the initializer.
!FILENAME app/models/user.rb
class User < ActiveRecord::Base
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable
151
Rails Messaging
acts_as_messageable
def mailboxer_name
self.name
end
def mailboxer_email(object)
self.email
end
end
The Controllers
We will be using two controllers to handle the messaging system.
MailboxController - This controller will hold our top-level folders (inbox, sentbox and trash)
and be responsible for displaying the messages in each of this folders.
ConversationsController - This controller will handle various actions including creating
conversations, replies and deletion of messages etc.
Let's start by creating the mailbox controller
$ rails g controller mailbox
We will then add some custom routes for inbox, sentbox and trash in our routes.rb file.
!FILENAME config/routes.rb
Rails.application.routes.draw do
devise_for :users
root 'welcome#index'
# mailbox folder routes
get "mailbox/inbox" => "mailbox#inbox", as: :mailbox_inbox
get "mailbox/sent" => "mailbox#sent", as: :mailbox_sent
get "mailbox/trash" => "mailbox#trash", as: :mailbox_trash
end
Let's define this methods in our mailbox controller.
!FILENAME app/controllers/mailbox_controller.rb
class MailboxController < ApplicationController
before_action :authenticate_user!
def inbox
@inbox = mailbox.inbox
@active = :inbox
end
def sent
@sent = mailbox.sentbox
@active = :sent
152
Rails Messaging
end
def trash
@trash = mailbox.trash
@active = :trash
end
end
We will use the @active variable to highlight the currently selected folder in our view. We can now
define the mailbox method as a helper method in our application_controller.rb file.
!FILENAME app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
# [...]
helper_method :mailbox
private
def mailbox
@mailbox ||= current_user.mailbox
end
protected
# [...]
end
Let's now create a view file for each of the actions in the mailbox controller. Each of this will share
the same partial to navigate between the mailbox folders.
!FILENAME app/views/mailbox/inbox.html.erb
<%= render partial: 'mailbox/folder_view' %>
!FILENAME app/views/mailbox/sent.html.erb
<%= render partial: 'mailbox/folder_view' %>
!FILENAME app/views/mailbox/trash.html.erb
<%= render partial: 'mailbox/folder_view' %>
Let's create the folder_view partial, in our mailbox folder, create a new file name it
_folder_view.html.erb and paste the following contents.
!FILENAME app/views/mailbox/_folder_view.html.erb
<div class="row">
<div class="spacer"></div>
<div class="col-md-12">
<!-- we'll configure this to compose new conversations later -->
<%= link_to "Compose", "#", class: "btn btn-success" %>
<div class="spacer"></div>
</div>
153
Rails Messaging
<div class="col-md-4">
<div class="panel panel-default">
<div class="panel-body">
<%= render 'mailbox/folders' %>
</div>
</div>
</div>
<div class="col-md-8">
<div class="panel panel-default">
<div class="panel-body">
<!-- individual conversations will show here -->
</div>
</div>
</div>
</div>
Inside here, we also make use of another partial called _folders. Let's also, in the same mailbox
folde, create a new file _folders.html.erb and have the following contents.
!FILENAME app/views/mailbox/_folders.html.erb
<ul class="nav nav-pills nav-stacked">
<li class="<%= active_page(:inbox) %>">
<%= link_to mailbox_inbox_path do %>
<span class="label label-danger pull-right"><%=unread_messag
es_count%></span>
<em class="fa fa-inbox fa-lg"></em>
<span>Inbox</span>
<% end %>
</li>
<li class="<%= active_page(:sent) %>">
<%= link_to mailbox_sent_path do %>
<em class="fa fa-paper-plane-o fa-lg"></em>
<span>Sent</span>
<% end %>
</li>
<li class="<%= active_page(:trash) %>">
<%= link_to mailbox_trash_path do %>
<em class="fa fa-trash-o fa-lg"></em>
<span>Trash</span>
<% end %>
</li>
</ul>
Remember the @active instance variable we instantiated in our mailbox controller? Let's make a
helper method that makes use of that variable to highlight the current page. In our
application_helper.rb file add the following method.
!FILENAME app/helpers/application_helper.rb
154
Rails Messaging
module ApplicationHelper
# [...]
def active_page(active_page)
@active == active_page ? "active" : ""
end
end
For our inbox, it would be a good idea to display the number of unread messages. We will define a
helper method in our mailbox_helper.rb file in the helpers directory.
!FILENAME app/helpers/mailbox_helper.rb
module MailboxHelper
def unread_messages_count
# how to get the number of unread messages for the current user
# using mailboxer
mailbox.inbox(:unread => true).count(:id, :distinct => true)
end
end
We can now add a link in our navigation bar to link to our inbox page.
!FILENAME app/views/layous/_nav.html.erb
<!-- [...] -->
<div id="navbar" class="navbar-collapse collapse">
<ul class="nav navbar-nav pull-right">
<% if user_signed_in? %>
<li><%= link_to "Hello, #{current_user.name}", "#" %></li
>
<li><%= link_to "Inbox", mailbox_inbox_path %></li>
<li><%= link_to "Logout", destroy_user_session_path, met
hod: :delete %></li>
<% else %>
<li class="active"><%= link_to "Login", new_user_session
_path %></li>
<li><%= link_to "Sign up", new_user_registration_path %>
</li>
<% end %>
</ul>
</div><!--/.nav-collapse -->
<!-- [...] -->
Clicking the inbox link should take you to our inbox page with navigation already in place for the
inbox, sent and trash folders.
Conversations
With our mailbox ready to show messages from our inbox, sent and trash folders, it's now time
we create functionality to create and send new messages to other users. Let's go ahead and create our
second controller (conversations).
$ rails g controller conversations
155
Rails Messaging
Let's not forget to add a resources route for our conversations which will also hold other 3 member
routes to handle reply, trash and untrashing of messages.
!FILENAME config/routes.rb
Rails.application.routes.draw do
# [...]
# conversations
resources :conversations do
member do
post :reply
post :trash
post :untrash
end
end
end
With the conversations routes in place, let's edit our compose button link in our folder_view partial to
point to the new action of our conversations controller.
!FILENAME app/views/mailbox/_folder_view.html.erb
<div class="row">
<div class="spacer"></div>
<div class="col-md-12">
<%= link_to "Compose", new_conversation_path, class: "btn btn-su
ccess" %>
<div class="spacer"></div>
</div>
<!-- [...] -->
</div>
We now need to create the new method in our conversations controller and its corresponding view file
!FILENAME app/controllers/conversations_controller.rb
class ConversationsController < ApplicationController
before_action :authenticate_user!
def new
end
end
!FILENAME app/views/conversations/new.html.erb
<%= render partial: 'mailbox/folder_view', locals: { is_conversation:
true } %>
Notice we are still using our folder_view partial which is in our mailbox folder. This partial
will be responsible for displaying different contents based on the current view. To start with, we are
passing in a locale called is_conversation. When this is set to true, we will render the form to
create new conversations and messages, else we will show contents for each mailbox folder.
In our views folder, conversations folder, create a form partial called _form.html.erb, that will
156
Rails Messaging
Rails Messaging
false } %>
With this, clicking on the Compose button on either page should take you to the new view of our
conversations controller and you should have a beautiful form ready to send messages
With our form up and running, we need to implement the create action in our conversations controller
to save the messages to the database. We call Mailboxer's send_message method on the current user
and that takes in an array of recipients, the message body and message subject after which we redirect
to the show action of our conversations controller.
!FILENAME app/controllers/conversations_controller.rb
class ConversationsController < ApplicationController
before_action :authenticate_user!
def new
end
def create
recipients = User.where(id: conversation_params[:recipients])
conversation = current_user.send_message(recipients, conversatio
n_params[:body], conversation_params[:subject]).conversation
flash[:success] = "Your message was successfully sent!"
redirect_to conversation_path(conversation)
end
def show
@receipts = conversation.receipts_for(current_user)
# mark conversation as read
conversation.mark_as_read(current_user)
end
private
def conversation_params
params.require(:conversation).permit(:subject, :body,recipients:
[])
end
end
As you can see in the show action, we query all messages in the current conversation for the current
user and store that in the @reciepts variable. Also, the conversation is actually a helper method we
need to define in our application controller to help us get the current conversation.
!FILENAME app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
# [...]
helper_method :mailbox, :conversation
private
# [...]
def conversation
158
Rails Messaging
Rails Messaging
Rails Messaging
:body])
flash[:notice] = "Your reply message was successfully sent!"
redirect_to conversation_path(conversation)
end
private
# [...]
def message_params
params.require(:message).permit(:body, :subject)
end
end
Mailboxer's reply_to_conversation method makes replying to conversations a breeze. It
takes in a conversation and the message body and optionally a subject as arguments. If you want users
to change the subject during reply, then remember to add the subject text field in the reply form and
also in the reply_to_conversation method above as the third argument.
Displaying messages
Our mailbox controller views are lonely and we need to show contents in each of them from which
users can click on a snippet and view the full message. In this folders, we want to show a snippet of
the last message in each unique conversation.
Lets create a new partial conversation inside our conversations folder name it
_conversation.html.erb
!FILENAME app/views/conversations/_conversation.html.erb
<div class="media">
<div class="media-left">
<a href="#">
<img class="media-object" src="http://placehold.it/64x64" alt=
"...">
</a>
</div>
<div class="media-body">
<h4 class="media-heading">
<%= conversation.originator.name %> <br>
<small><b>Subject: </b><%= conversation.subject %></small><br>
<small><b>Date: </b><%= conversation.messages.last.created_at
.strftime("%A, %b %d, %Y at %I:%M%p") %></small>
</h4>
<%= truncate conversation.messages.last.body, length: 145 %>
<%= link_to "View", conversation_path(conversation) %>
</div>
</div>
Updating our inbox, sent and trash views
!FILENAME app/views/inbox.html.erb
<%= render partial: 'mailbox/folder_view', locals: { is_conversation:
false, messages: @inbox } %>
161
Rails Messaging
!FILENAME app/views/sent.html.erb
<%= render partial: 'mailbox/folder_view', locals: { is_conversation:
false, messages: @sent } %>
!FILENAME app/views/trash.html.erb
<%= render partial: 'mailbox/folder_view', locals: { is_conversation:
false, messages: @trash } %>
In all of our three views, we pass in messages as a locale to our folder_view partial which represents
messages from either our inbox, sentbox or trash. Finally lets update our folder_view
partial to use the messages locale and consequently render the conversation partial we just created.
!FILENAME app/views/mailbox/_folder_view.html.erb
<div class="row">
<!--[...]-->
<div class="col-md-8">
<div class="panel panel-default">
<div class="panel-body">
<% if is_conversation %>
<%= render 'conversations/form' %>
<% else %>
<%= render partial: 'conversations/conversation', collec
tion: messages %>
<% end %>
</div>
</div>
</div>
</div>
You should now be able to see your messages, if any, when you click the inbox and sent links in any
view. One last part is remaining, as you can guess, that is ability to delete messages and send them to
the trash and also ability to undelete messages which we might have deleted by mistake.
Rails Messaging
163
Rails Messaging
def untrash
conversation.untrash(current_user)
redirect_to mailbox_inbox_path
end
private
# [..]
end
As you can see Mailboxer provides handful methods move_to_trash to send messages to the current
users's trash box and untrash to move back the message back to the users inbox.
Additional features
As you many have noted, the current method of selecting recipients is not very efficient especially
when the number of users increase, finding a user can become way too tedious. We can improve on
this by use of a jQuery plugin called Chosen which makes it simple to select items from a large list
and is more user-friendly. Fortunately for us, there is a chosen-rails gem that makes integrating
this plugin into Rails app very easy.
To Install the gem in our rails app, add chosen-rails to your Gemfile and run bundle install.
!FILENAME GEMFILE
# [...]
gem 'chosen-rails'
We then need to add Chosen to application.js and application.css.scss files:
!FILENAME app/assets/javascripts/application.js
# [....]
//= require jquery
//= require jquery_ujs
//= require chosen-jquery
//= require turbolinks
//= require_tree .
!FILENAME app/assets/stylesheets/application.css
/* [...] */
*
*= require_tree .
*= require chosen
*= require_self
*/
@import 'bootstrap';
@import 'bootstrap/theme';
We then add the chosen-select class to our select dropdown in our conversation's form select field.
!FILENAME app/views/conversations/_form.html.erb
164
Rails Messaging
Wrap up
At this stage the application has the core functionality of the mailboxer gem implemented.
165
Your assignment
You are assigned by your Project Manager to work on an application that is totally undocumented and
not actively maintained. Your and your teammates task is to get the code base, deploy it to a staging
server and get it to a state where adding new functionality is possible.
A big part of your work will be to cover the application with tests.
1. Write Acceptance tests for the entire workflow using either RSpec OR Cucumber
2. Write Unit tests for the models using RSpec
3. Set up an automated way of deploying the software.
You are asked to use a service of your choice in order to get metrics on how well the code is tested.
You are free to use any service you want in order to document your work.
Objective
The main objective for this challenge is to practice the workflow that we want to use in our
projects with focus on:
1.
2.
3.
4.
Also, setting up a tool that allows you to plan your work (Pivotal Tracker or Waffle.io) in this projects
is not a bad idea.
This is a non-trivial application and even if the gems we will be using will provide most of the
functionality we will need, it is still a hard challenge. The only way you will succeed is if you pay
close attention to details and focus on the right tasks in the right time.
At some point during this week you will be asked to explain your workflow to the coaches. We will
evaluate each and every commit you make to GitHub and we will pay close attention to the sequence
in which you do things.
166
Documenting all aspects of your application in your README file is, of course, important.
As with every other material, there might be errors in the provided code. It's up to you to find
and correct them.
Finally, remember, Its not just about the code. This challenge is about practicing the workflow.
Resources
Deployed application
https://ca-mailboxer.herokuapp.com/
Code base
https://github.com/CraftAcademy/rails_messaging
167
DELETE /users(.:format)
devise/registrations#destroy
welcome_index GET /welcome/index(.:format)
welcome#index
root GET /
welcome#index
mailbox_inbox GET /mailbox/inbox(.:format)
mailbox#inbox
mailbox_sent GET /mailbox/sent(.:format)
mailbox#sent
mailbox_trash GET /mailbox/trash(.:format)
mailbox#trash
reply_conversation POST /conversations/:id/reply(.:format)
conversations#reply
trash_conversation POST /conversations/:id/trash(.:format)
conversations#trash
untrash_conversation POST /conversations/:id/untrash(.:format)
conversations#untrash
conversations GET /conversations(.:format)
conversations#index
POST /conversations(.:format)
conversations#create
new_conversation GET /conversations/new(.:format)
conversations#new
edit_conversation GET /conversations/:id/edit(.:format)
conversations#edit
conversation GET /conversations/:id(.:format)
conversations#show
PATCH /conversations/:id(.:format)
conversations#update
PUT /conversations/:id(.:format)
conversations#update
DELETE /conversations/:id(.:format)
conversations#destroy
Now, lets say that you have a user object stored in @user and want to access the edit user view in
your test. In this case you can tell capybara to go to that edit page with:
visit edit_user_registration_path(@user)
Or if you want to go to that users inbox:
visit mailbox_inbox_path
In this case you probably dont have to pass in the @user since we have a method in our
ApplicationController setting mailbox to the current_user.mailbox.
!FILENAME app/controllers/application_controller.rb
def mailbox
@mailbox ||= current_user.mailbox
end
current_user being a Devise method of course.
169
Warden helpers
The gem we are using for user authentication, Devise, is build upon Warden that you might remember
from the Slow Food challenge.
Using Warden/Devise methods to log in a user: Create a new file in the features/support
folder.
!FILENAME features/support/warden.rb
Warden.test_mode!
World Warden::Test::Helpers
After { Warden.test_reset! }
Now you can create a step def that look something like this:
Given(/^I am logged in as "([^"]*)"$/) do |name|
user = User.find_by(name: name)
login_as(user, scope: :user)
end
With this method you are NOT required to hardcode and use the users password. You need to
understand that the controller action is not being hit with this way of logging in the user so in order to
go to a specific page you will need to add a separate step. Like..
And I am on the "inbox page"
...or something.
For logging out the user you can define a step def like this:
Given /^I log out$/ do
logout
end
Much cleaner and DRY.
Testing JavaScript
Add bothe Poltergeist and PhantomJS to your Gemfile.
!FILENAME Gemfile
gem 'poltergeist'
gem 'phantomjs', require: 'phantomjs/poltergeist'
In your features/support/env.rb add poltergeist as a dependancy.
!FILENAME features/support/env.rb
require 'capybara/poltergeist'
...
Capybara.javascript_driver = :poltergeist
With this setup you have access to a full set of Poltergeist commands you can use in your step
170
definitions. To activate the JavaScript driver on a scenario you have to tag your scenario with
@javascript.
@javascript
Scenario: Deleting a message
Given I am logged-in as "Daniel"
And I send a mail to "Jenny"
And I am on the "home page"
...
Mailboxer methods
You can use methods available by the Mailboxer gem in your step definitions to make them run faster.
Let's say you have a scenario like this:
Background:
Given following users exists
| name | email | password |
| Jenny | jenny@ranom.com | password |
| Daniel | daniel@random.com | password |
Scenario: Deleting a message
Given I am logged in as "Daniel"
And I send a mail to "Jenny"
And I am on the "home page"
And I click on the "Logout" link
Given I am logged-in as "Jenny"
And I am on the "home page"
And I click on the "Inbox" link
Then I should have "1" messages
And I click on the "View" link
And I click on the "Move to trash" link
Then I should have "0" messages
You can use some mailboxer methods
Given(/^I am logged in as "([^"]*)"$/) do |name|
@user = User.find_by(name: name)
login_as(@user, scope: :user)
end
And(/^I send a mail to "([^"]*)"$/) do |name|
@receiver = User.find_by(name: name)
@user.send_message(@receiver, 'Lorem ipsum...', 'Subject')
end
171
172
Objectives
Put all of the skills you have picked up to use in a group project.
1.
2.
3.
4.
5.
Requirements
Build and deploy a SAAS (Software as as Service) application.
Implement user authentication and authorization (Devise, CanCanCan)
Implement OAuth authentication
Implement some sort of market place functionality (services or products)
Make use of a payment gateway (PayPal, Klarna, Stripe)
Make use of the geocoding and maps (Google Maps, OpenStreet Map) for vendors or service
providers.
Implement a rating system (for products, vendors or something else)
Set up a third party API integration (TraficLab, Facebook, etc)
Execution
The MidCourse Project is about execution: planning your work, collaboration as a team, doing the
right thing at the right time and always have your priorities straight.
1. How do we start our project?
2. What is the domain?
3. Do we start with creating a high level description or filling the backlog with user stories and
chores. Or both?
4. Shall we make use of LoFi's?
5. What framework shall we use? Rails or Sinatra? Or something else?
6. Who does what and when?
7. Should we have a main repository?
Remember that this is a simulation of a real enterprise project and even if there is a lot of uncertainties
in terms of client requirements you can treat this as a project for a start-up. You have the power to
make the decisions. The above mentioned requirements are suggestions to make the project an
interesting learning experience.
This project will become part of your personal portfolio so don't aim too high but do make sure that
there is enough complexity for an reviewer to find the application interesting.
173
174
Project Schedule
Project Schedule
Project Schedule
The MidCourse Project is a 7-day task. During this week you need to work as a regular project team
and plan and execute daily stand-ups and project planning meetings on your own.
Suggested schedule
Start each day with a stand-up to access each team members progress and to plan your daily activities.
Assign a team member to be responsible for deployment. That person can have multiple heroku
sources on his local repo or, even better, set up Travis (or what have you) to deploy to different
servers depending on what branch is being used (you should work on development branch.
End each day with a deploy to a development server and a short demo in a stand up format. Plan the
evening's or the next day's activities.
Description
For user stories, provide enough detail for estimating how much work will be required to
implement the story. Focus on who the feature is for, what users want to accomplish, and
Description
why. Don't describe how the feature should be developed. Do provide sufficient details
so that your team can write tasks and test cases to implement the item.
Provide the criteria to be met before the bug or user story can be closed.
Before work begins, describe the customer acceptance criteria as clearly as possible.
Acceptance Conversations between the team and customers to define the acceptance criteria will
Criteria
help ensure that your team understands your customer's expectations. The acceptance
criteria can be used as the basis for acceptance tests so that you can more effectively
evaluate whether an item has been satisfactorily completed.
The area of customer value addressed by the epic, feature, requirement, or backlog item.
Values include:
175
Project Schedule
Value area Architectural: Technical services to implement business features that deliver solution
Business: Services that fulfill customers or stakeholder needs that directly deliver
customer value to support the business (Default)
Story
Estimate the amount of work required to complete a user story using any unit of
Points
measurement your team prefers, such as t-shirt size, story points, or time.
A subjective rating of the user story, feature, or requirement as it relates to the business.
Allowed values are
1. Product cannot ship without the feature.
Priority
2: Product cannot ship without the feature, but it doesn't have to be addressed
immediately.
3: Implementation of the feature is optional based on resources, time, and risk.
A subjective rating of the relative uncertainty around the successful completion of a user
story. Allowed values are:
Risk
1 - High
2 - Medium
3 - Low
176
177
Think of Ionic as the front-end UI framework that handles all of the look and feel and UI interactions
your app needs in order to be compelling. Kind of like Bootstrap for Native, but with support for a
broad range of common native mobile components, slick animations, and beautiful design.
Unlike a responsive framework, Ionic comes with very native-styled mobile UI elements and layouts
that you would get with a native SDK on iOS or Android but didnt really exist before on the web.
Ionic also gives you some opinionated but powerful ways to build mobile applications that eclipse
existing HTML5 development frameworks.
178
Getting started
Getting started
Getting started
Prerequisites
We will build a simple application that allows the user to enter his weight and height and calculate his
personal BMI value. This requires you to have completed the previously described BMI Challenge. If
you haven't completed that challenge, please go back and do it before you proceed or grab the source
code to get the necessary files from the Extras chapter of this walkthrough. You will need to get your
hands on person.js and bmi_calculator.js.
Getting started
At this point, inside the project folder, you should initialize a git repository.
$ git init
$ git add .
$ git commit -am "scaffold new Ionic tabs application"
You should also create a new repository on GitHub and add it as a remote. Git flow is outside the
scope of this walkthrough but do make use of version control when working on this project.
180
Getting started
181
182
The folder I want you to focus on is the www folder. That is where we will do most of our work. But
let's start with adding the following two lines to your .gitignore file. It is located in the project
root.
!FILENAME .gitignore
.idea/
.DS_Store
This ensures that no unnecessary files tracked under version control.
Find app.js in the www/js folder and locate the following parts:
!FILENAME www/js/app.js
.state('tab.dash', {
url: '/dash',
views: {
'tab-dash': {
templateUrl: 'templates/tab-dash.html',
controller: 'DashCtrl'
}
}
})
183
.state('tab.chats', {
url: '/chats',
views: {
'tab-chats': {
templateUrl: 'templates/tab-chats.html',
controller: 'ChatsCtrl'
}
}
})
.state('tab.chat-detail', {
url: '/chats/:chatId',
views: {
'tab-chats': {
templateUrl: 'templates/chat-detail.html',
controller: 'ChatDetailCtrl'
}
}
})
.state('tab.account', {
url: '/account',
views: {
'tab-account': {
templateUrl: 'templates/tab-account.html',
controller: 'AccountCtrl'
}
}
});
Delete that entire block of code. Now head over to the controllers.js file and delete the
following code:
!FILENAME www/js/controllers.js
.controller('DashCtrl', function($scope) {})
.controller('ChatsCtrl', function($scope, Chats) {
// With the new view caching in Ionic, Controllers are only called
// when they are recreated or on app start, instead of every page
change.
// To listen for when this page is active (for example, to refresh
data),
// listen for the $ionicView.enter event:
//
//$scope.$on('$ionicView.enter', function(e) {
//});
$scope.chats = Chats.all();
$scope.remove = function(chat) {
Chats.remove(chat);
};
})
184
www/templates/chat-detail.html
www/templates/tab-account.html
www/templates/tab-chats.html
www/templates/tab-dash.html
The only file we want to keep is the templates/tabs.html
Make sure you commit your changes with:
$ git add .
$ git commit -am "clean up scaffolded code"
Adding views
Now that we have cleaned up the code base we are ready to add our own stuff. We'll be making use of
two tabs. One About tab that will route us to an "About page", and one "BMI Calculator" tab that will
be displaying the view where we will be doing the calculations.
Let's start with creating the About page.
Open up the templates/tabs.html file again and add the markup for the About tab
!FILENAME www/templates/tabs.html
<!-- AboutTab -->
<ion-tab title="About" icon-off="ion-ios-compose-outline" icon-on="i
on-ios-compose" href="#/tab/about">
<ion-nav-view name="tab-about"></ion-nav-view>
</ion-tab>
Create a route by adding this code to the js/app.js where we define $stateProvider
!FILENAME www/js/app.js
[...]
.state('tab.about', {
url: '/about',
views: {
'tab-about': {
templateUrl: 'templates/about/about.html',
controller: 'AboutController'
}
}
})
Also, in the same file, at the bottom of the $stateProvider block, change the following code to
point to the About page as default.
!FILENAME www/js/app.js
[...]
// if none of the above states are matched, use this as the fallback
$urlRouterProvider.otherwise('/tab/about');
Okay, time to add a HTML template we want to display then that tab is called upon.
186
In your templates folder create a new folder named about and add a new template inside that
folder. Call it about.html.
(Note, I prefer to keep my templates in separate folders for better readability)
$ mkdir www/templates/about
$ touch www/templates/about/about.html
Head over to the controllers.js file in your www/js folder and create an
AboutController
.controller('AboutController', function () {
});
And finally, open up the about.html template and add some content.
!FILENAME www/templates/about/about.html
<ion-view title="BMI Mobile">
<ion-content>
<div class="row">
<div class="col">
<p>Craft Academy Demo application porting the BMI calculator
to Ionic.</p>
</div>
</div>
</ion-content>
</ion-view>
At this stage you can start the server again (you can keep it running in the background on a separate
Terminal tab. It is reloading your code as you make the changes) and head over to the browser. You
should see your new About page as the default view.
187
188
Let's open the www/templates/tabs.html file and add the markup for the tab:
!FILENAME www/templates/tabs.html
<!-- BmiTab -->
<ion-tab title="BMI Calculator" icon-off="ion-ios-compose-outline" i
con-on="ion-ios-compose" href="#/tab/bmi">
<ion-nav-view name="tab-bmi"></ion-nav-view>
</ion-tab>
Create the route in www/js/app.js by adding the following ABOVE the About route (the reason
for that is that you might mess up some semicolon settings at the end of the block)
!FILENAME www/js/app.js
[...]
.state('tab.bmi', {
url: '/bmi',
views: {
'tab-bmi': {
templateUrl: 'templates/calculator/calculator.html',
controller: 'BmiController'
}
}
})
.state('tab.about', {
url: '/about',
views: {
'tab-about': {
templateUrl: 'templates/about/about.html',
controller: 'AboutController'
}
}
});
Now, let's create the controller in www/js/controllers.js. (Again, watch the semicolon!)
!FILENAME www/js/controllers.js
189
.controller('BmiController', function() {
});
And finally, let's create the template.
$ mkdir www/templates/calculator
$ touch www/templates/calculator/calculator.html
Open the file and add some basic content:
!FILENAME www/templates/calculator/calculator.html
<ion-view title="BMI Calculator">
<ion-content>
<div class="row">
<div class="col">
</div>
</div>
</ion-content>
</ion-view>
At this point you should be able to navigate between the two tabs we've set up if you visit your app in
the browser.
190
191
Adding functionality
Adding functionality
Adding functionality
Okay, let's move on to implementing the BMI Calculator.
Copy the person.js and bmi_calculator.js files into your www/js folder. If you don't
have the source files, you can find them in the Extras chapter of this walkthrough.
Reference those files in your main template file (www/index.html) in the block where you are
including your other js files.
!FILENAME www/index.html
<!-- your app's js -->
<script src="js/app.js"></script>
<script src="js/controllers.js"></script>
<script src="js/services.js"></script>
<script src="js/person.js"></script>
<script src="js/bmi_calculator.js"></script>
Let's create a form on the calculator.html template that will allow the user to input his/her
weight and height and return the calculated values.
!FILENAME www/templates/calculator/calculator.html
[...]
<div class="list">
<label class="item item-input">
<input type="number" class="item item-input" placeholder="Weight"
ng-model="data.weight">
</label>
<label class="item item-input">
<input type="number" class="item item-input" placeholder="Height"
ng-model="data.height">
</label>
<button class="button button-full button-calm" ng-click="calculate
BMI()">Calculate</button>
</div>
When submitted, the form will send the values from the input fields and store it in the controllers
$scope as data and execute the calculateBMI() function. Given that there is a function
defined. There isn't at the moment, so let's define it.
Head over to the BmiController in your www/js/controllers.js file.
We need to add $scope to the controller and define the calculateBMI() function.
!FILENAME www/js/controllers.js
.controller('BmiController', function($scope) {
$scope.data = {};
192
Adding functionality
$scope.calculateBMI = function() {
var person = new Person({
weight: $scope.data.weight,
height: $scope.data.height
});
person.calculate_bmi_met();
$scope.person = person;
};
});
This will make the object person accessible in the HTML template for us to show. Lets add the
following code to calculator.html
!FILENAME www/templates/calculator/calculator.html
[...]
<div class="card" ng-if="person">
<div class="item item-divider">
BMI Calculation
</div>
<div class="item item-text-wrap">
<p>Person: Weight {{person.weight}}, Height {{person.height}}</p>
<p>BMI: {{person.bmiValue}}</p>
<p><strong>You are {{person.bmiMessage}}</strong></p>
</div>
</div>
Some explanation
1. The ng-if="person" makes sure that the div element is only displayed IF the
$scope.person object defined.
2. The {{}} brackets are a way to display variables/objects passed in to the template from the
controller.
You can head over to the browser and try it out. Remember that we have set up the metric method of
calculation the users BMI. There is a method to calculate BMI using the imperial method in the
bmi_calculator.js code. You can easily make the switch or add functionality that allows the
user to switch between the two methods.
That is however outside the scope of this walkthrough. ;-) The final app should look something like
this.
193
Adding functionality
Wrap up
There are plenty of other little things you can do in order to update the UI and add functionality to this
little application.
One final thing before we call it quits is to publish your app to Ionic View.
194
Adding functionality
Head over to the Ionic platform signup page to get signed up and to get your Ionic ID.
195
Adding functionality
196
};
function setBMIMessage (obj, value){
if (obj.bmiValue < 18.5) {
obj.bmiMessage = "Underweight"
}
if (obj.bmiValue > 18.5 && obj.bmiValue < 25) {
obj.bmiMessage = "Normal"
}
if (obj.bmiValue > 25 && obj.bmiValue < 30) {
obj.bmiMessage = "Overweight"
}
if (obj.bmiValue > 30) {
obj.bmiMessage = "Obese"
}
}
198
Learning objectives
Learn how to build an API using Ruby on Rails
Learn about testing API endpoints with RSpec using so called request specs
Learn about CORS
Learn how to authenticate users from an Ionic application
Learn how to set consume an API from a mobile client
Learn about Factories and Controllers in AngularJS
Make use of knowledge of JavaScript and Jasmine to build application logic
Extra challenge
Learn about acceptance testing using Protractor
199
The logic
The logic
The logic - Step 1
Let's start at the unit level.
As an athlete,
In order to get to know my results from the Cooper test
I want to be able to input my distance and get a result from the nor
mative data table
We will be doing all calculations on the client side and only save the results to our centralized
database (back-end). This means that we will be working defining our logic in JavaScript. This means
that we will be using Jasmine for Unit testing.
As a first step we want to make sure that our units are working.
Here are the steps you need to complete at this stage:
Create a new project folder
Set up Jasmine (see this section).
Write a JavaScript program that returns an assessment given the users gender, age and
completed distance. Store that program in cooper.js
Make the software accessible in your browsers console
You DON'T need to create a UI at this point.
You need to create a Person object with a minimum of 2 attributes in order to be able to make the
calculation - gender and age are required to be present.
function Person(attr) {
this.gender = attr.gender;
this.age = attr.age;
};
Then, you need to add a function that takes the distance of the 12-minute run and makes the
calculation and fetches the results depending on gender.
There are several ways to achieve that. If you need some inspiration you can review this repo where
you'll find two solutions. One using a case method and the second one that uses array index and
ranges to set the message (written by Raoul).
Make sure you review the specs in order to understand how those method differ and how they can be
implemented.
Remember, try to write your own implementation rather than just copying the provided code. It's all
about learning. When the course is over, you'll be on your own solving problems.
Once you are done, put that code aside. Even though it is the core of the application, we will not
be using it for the next step - the back end. That part does not care about HOW we come up
with the results, just THAT we do. The calculation will be used in our mobile app - not in the
back-end.
200
The logic
201
The Back-end
The Back-end
The Back-end - Step 2
We will be using Ruby on Rails as the stack for our back-end.
The challenge is to set up an API-only application that will make it possible to store information
about users and their historical data.
We will be using RSpec as out testing framework and PostgreSQL as our database.
Note that we WILL NOT be using Cucumber, so you don't have to install that framework. We
are also using Rails 5 in this walkthrough.
Let's go ahead and scaffold our application
rails new cooper_api --api --database=postgresql --skip-test --skipbundle
This will do a couple of things for you:
Configure your application to start with a limited set of middleware than normal.
Make ApplicationController inherit from ActionController::API instead of
ActionController::Base. As with middleware, this will leave out any Action Controller modules
that provide functionalities primarily used by browser applications.
Configure the generators to skip generating views, helpers and assets when you generate a new
resource.
--database=postgresql selects PostgreSQL as the database
--skip-test option skips configuring for the default testing tool.
--skip-bundle option prevents the generator from running bundle install automatically.
Next, update your Gemfile to have the following:
source 'https://rubygems.org'
ruby '2.3.1'
gem 'rails', '~> 5.0.0', '>= 5.0.0.1'
gem 'pg', '~> 0.18'
gem 'puma', '~> 3.0'
gem 'jbuilder', '~> 2.5'
group :development, :test do
gem 'pry'
gem 'pry-byebug'
end
group :development do
gem 'listen', '~> 3.0.5'
gem 'spring'
gem 'spring-watcher-listen', '~> 2.0.0'
end
202
The Back-end
We also want to add the rack-cors gem to allow external clients to access our application. Add the
dependency to your Gemfile.
!FILENAME Gemfile
# Use Rack CORS for handling Cross-Origin Resource Sharing (CORS),
# making cross-origin AJAX possible
gem 'rack-cors', require: 'rack/cors'
Put something like the code below in config/application.rb of your Rails application. This
will allow GET, POST, PUT and DELETE requests from any origin on any resource.
!FILENAME config/application.rb
module CooperApi
class Application < Rails::Application
# [...]
config.middleware.insert_before 0, Rack::Cors do
allow do
origins '*'
resource '*', headers: :any, methods: [:get, :post, :put, :d
elete]
end
end
end
end
There are plenty of settings you can add to enhance security of your application. Read about it in the
rack-cors and devise_token_auth gem documentation.
Next, we are now going to setup our testing framework.
Update your Gemfile with the following gems,
group :development, :test do
gem 'rspec-rails'
gem 'shoulda-matchers'
gem 'factory_girl_rails'
# [...]
end
Remember to run bundle install everytime you update your Gemfile.
Run rails generate rspec:install to install rspec for your rails project. Update the
following file with respective code provided below:
spec/rails_helper.rb
ENV['RAILS_ENV'] ||= 'test'
require File.expand_path('../../config/environment', __FILE__)
abort('The Rails environment is running in production mode!') if Rai
ls.env.production?
require 'spec_helper'
require 'rspec/rails'
203
The Back-end
ActiveRecord::Migration.maintain_test_schema!
Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f }
RSpec.configure do |config|
config.fixture_path = "#{::Rails.root}/spec/fixtures"
config.use_transactional_fixtures = true
config.infer_spec_type_from_file_location!
config.filter_rails_from_backtrace!
end
spec/spec_helper.rb
RSpec.configure do |config|
config.expect_with :rspec do |expectations|
expectations.include_chain_clauses_in_custom_matcher_description
s = true
end
config.mock_with :rspec do |mocks|
mocks.verify_partial_doubles = true
end
config.shared_context_metadata_behavior = :apply_to_host_groups
end
.rspec
--color
--require rails_helper
Create the following files:
spec/support/factory_girl.rb
RSpec.configure do |config|
config.include FactoryGirl::Syntax::Methods
end
spec/support/shoulda_matcher.rb
Shoulda::Matchers.configure do |config|
config.integrate do |with|
with.test_framework :rspec
with.library :rails
endend
RSpec.configure do |config|
config.include(Shoulda::Matchers::ActiveRecord, type: :model)
end
That's all for the setup. Next up, we will test-drive the creation of a test endpoint.
204
The Back-end
The Back-end
$ touch app/controllers/api/v0/ping_controller.rb
We will let it inherit from our modified ApplicationController
!FILENAME app\/controllers\/api\/v0\/ping_controller.rb
class Api::V0::PingController < ApplicationController
end
If we run the test now, the error about undefined constant Api::V0::PingController is gone
and RSpec should now throw you a No route matches [GET] "/api/v0/ping" error.
In your routes.rb create an API namespace and add V0 within it. Nested in that namespace we
want to add a :ping resource with one single :index action.
!FILENAME config\/routes.rb
Rails.application.routes.draw do
namespace :api do
namespace :v0 do
resources :ping, only: [:index], constraints: { format: 'json'
}
end
end
end
Run rake routes in your terminal to see if the route has been added properly.
With our route in place, the test now throws the following error
AbstractController::ActionNotFound:
The action 'index' could not be found for Api::V0::PingController
In order to make this spec to pass, we need to add an index method to the
Api::V0::PingController. When called, that method will respond with Json object with a
single entry: {message: 'Pong'}.
!FILENAME app\/controllers\/api\/v0\/ping_controller.rb
class Api::V0::PingController < ApplicationController
def index
render json: { message: 'Pong' }
end
end
Does it work? Fire up your server with rails s and visit
http://localhost:3000/api/v0/ping
The Back-end
The Back-end
FactoryGirl.define do
factory :user do
email 'user@random.com'
password 'password'
password_confirmation 'password'
end
end
You can add more attributes to the User factory if you like, we added just the minimal required
attributes at the moment.
Let's add a spec for the User factory we just created.
!FILENAME spec/models/user_spec.rb
require 'rails_helper'
RSpec.describe User, type: :model do
it 'should have valid Factory' do
expect(FactoryGirl.create(:user)).to be_valid
end
end
Now, we can add some basic model specs for User that will test the Devise setup.
!FILENAME spec\/models\/user_spec.rb
RSpec.describe User, type: :model do
# [...]
describe 'Database table' do
it { is_expected.to have_db_column :id }
it { is_expected.to have_db_column :provider }
it { is_expected.to have_db_column :uid }
it { is_expected.to have_db_column :encrypted_password }
it { is_expected.to have_db_column :reset_password_token }
it { is_expected.to have_db_column :reset_password_sent_at }
it { is_expected.to have_db_column :remember_created_at }
it { is_expected.to have_db_column :sign_in_count }
it { is_expected.to have_db_column :current_sign_in_at }
it { is_expected.to have_db_column :last_sign_in_at }
it { is_expected.to have_db_column :current_sign_in_ip }
it { is_expected.to have_db_column :last_sign_in_ip }
it { is_expected.to have_db_column :confirmation_token }
it { is_expected.to have_db_column :confirmed_at }
it { is_expected.to have_db_column :confirmation_sent_at }
it { is_expected.to have_db_column :unconfirmed_email }
it { is_expected.to have_db_column :nickname }
it { is_expected.to have_db_column :image }
it { is_expected.to have_db_column :email }
it { is_expected.to have_db_column :tokens }
it { is_expected.to have_db_column :created_at }
it { is_expected.to have_db_column :updated_at }
end
end
208
The Back-end
The Back-end
The Back-end
def response_json
JSON.parse(response.body)
end
end
RSpec.configure do |config|
config.include ResponseJSON
end
User registration
Okay, let's write some specs for user registration.
$ mkdir -p spec/requests/api/v1
$ touch spec/requests/api/v1/registrations_spec.rb
!FILENAME spec/requests/api/v1/registrations_spec.rb
RSpec.describe 'User Registration', type: :request do
let(:headers) { { HTTP_ACCEPT: 'application/json' } }
context 'with valid credentials' do
it 'returns a user and token' do
post '/api/v1/auth', params: {
email: 'example@craftacademy.se', password: 'password',
password_confirmation: 'password'
}, headers: headers
expect(response_json['status']).to eq 'success'
expect(response.status).to eq 200
end
end
context 'returns an error message when user submits' do
it 'non-matching password confirmation' do
post '/api/v1/auth', params: {
email: 'example@craftacademy.se', password: 'password',
password_confirmation: 'wrong_password'
}, headers: headers
expect(response_json['errors']['password_confirmation'])
.to eq ["doesn't match Password"]
expect(response.status).to eq 422
end
it 'an invalid email address' do
post '/api/v1/auth', params: {
email: 'example@craft', password: 'password',
password_confirmation: 'password'
}, headers: headers
expect(response_json['errors']['email']).to eq ['is not an ema
il']
211
The Back-end
User authentication
Let's write some specs for logging in.
$ touch spec/requests/api/v1/sessions_spec.rb
!FILENAME spec/requests/api/v1/sessions_spec.rb
RSpec.describe 'Sessions', type: :request do
let(:user) { FactoryGirl.create(:user) }
let(:headers) { { HTTP_ACCEPT: 'application/json' } }
describe 'POST /api/v1/auth/sign_in' do
it 'valid credentials returns user' do
post '/api/v1/auth/sign_in', params: {
email: user.email, password: user.password
}, headers: headers
expected_response = {
'data' => {
'id' => user.id, 'uid' => user.email, 'email' => user.emai
l,
'provider' => 'email', 'name' => nil, 'nickname' => nil,
'image' => nil
} }
expect(response_json).to eq expected_response
end
it 'invalid password returns error message' do
212
The Back-end
The Back-end
end
Run the new migration.
$ rails db:migrate --all
Add the following specs.
!FILENAME spec/models/user_spec.rb
RSpec.describe User, type: :model do
# [...]
describe 'Relations' do
it { is_expected.to have_many :performance_data }
end
end
And to the newly created spec/models/performance_data_spec.rb.
!FILENAME spec/models/performance_data_spec.rb
require 'rails_helper'
RSpec.describe PerformanceData, type: :model do
describe 'Database table' do
it { is_expected.to have_db_column :id }
it { is_expected.to have_db_column :data }
end
describe 'Relations' do
it { is_expected.to belong_to :user }
end
end
The controller
Let's create the controller we will use to create, update and retrieve users historical data.
This is where we will be performing our CRUD actions.
The first thing we want to be able to do is to save data.
Let's create a request spec and start adding functionality to our controller.
In the spec/requests/api/v1/ folder we want to create a new test file. Let's call it
performance_data_spec.rb. We can start with a simple test to see if our entry will be saved to
the database (it WILL fail at first, but that is the way we do it, right?)
!FILENAME spec/requests/api/v1/performance_data_spec.rb
RSpec.describe Api::V1::PerformanceDataController, type: :request do
let(:headers) { { HTTP_ACCEPT: 'application/json' } }
describe 'POST /api/v1/performance_data' do
214
The Back-end
The Back-end
ForbiddenAttributesError
Performance Data
POST /api/v1/performance_data/
creates a data entry (FAILED - 1)
Failures:
1) Performance Data POST /api/v1/performance_data/ creates a dat
a entry
Failure/Error: @data = PerformanceData.new(params[:performanc
e_data])
ActiveModel::ForbiddenAttributesError:
ActiveModel::ForbiddenAttributesError
# ./app/controllers/api/v1/performance_data_controller.rb:4:i
n `create'
....
That is due to our params not being whitelisted. Some explanation is in place. Also do through this
resource about Action Controller to fully understand what is going on in the controllers we create and
use in our application.
Action Controller parameters are forbidden to be used in Active Model mass assignments until they
have been whitelisted. Strong Parameters provides an interface for protecting attributes from end-user
assignment.
So we need to whitelist our params. There are many different approaches for doing that. For now we
will whitelist all params contained on the :performance_data key and update the
createmethod to use them. Modify the performance_data_controller.rb with this code.
!FILENAME app\/controllers\/api\/v1\/performance_data_controller.rb
class Api::V1::PerformanceDataController < ApplicationController
def create
@data = PerformanceData.new(performance_data_params)
if @data.save
render json: { message: 'all good' }
else
render json: { error: @data.errors.full_messages }
end
end
private
def performance_data_params
params.require(:performance_data).permit!
end
end
Note: We can use the permit! method for now, but there might be some security issues involved
with that. Can you figure out a better way?
216
The Back-end
Okay, at this stage if you run your tests again you'll get a different error
undefined method `data' for nil:NilClass
The problem is that we are not assigning a user to the created entry, thus resulting in the entry not
being created in the DB. The application does not know what user it should reference to the new
object.
We need to update out controller to retrieve that information. We can do that with the built in Devise
method authenticate_user!. We also need to add the user information to the
performance_data_params. This can be done with a merge! command. Review the
following code before you implement it in your performance_data_controller.rb.
!FILENAME app\/controllers\/api\/v1\/performance_data_controller.rb
class Api::V1::PerformanceDataController < ApplicationController
before_action :authenticate_api_v1_user!
def create
@data = PerformanceData.new(performance_data_params.merge(user:
current_api_v1_user))
if @data.save
render json: { message: 'all good' }
else
render json: { error: @data.errors.full_messages }
end
end
private
def performance_data_params
params.require(:performance_data).permit!
end
end
We also need to update our spec and create a user (using Factory Girl) and sent that users credentials
with the request (in headers). We will use the DeviseTokenAuth method
create_new_auth_token to generate the necessary credentials. Make sure to use your debugger
and break the test at some point to run this command on the user object manually to see it in action.
Anyway, your spec should look something like this.
!FILENAME spec/requests/api/v1/performance_data_spec.rb
RSpec.describe Api::V1::PerformanceDataController, type: :request do
let(:user) { FactoryGirl.create(:user) }
let(:credentials) { user.create_new_auth_token }
let(:headers) { { HTTP_ACCEPT: 'application/json' }.merge!(credent
ials) }
describe 'POST /api/v1/performance_data' do
it 'creates a data entry' do
post '/api/v1/performance_data', params: {
217
The Back-end
The Back-end
Go ahead and run this spec just to get a friendly reminder that there is no such route as GET
/api/v1/performance_data/. Let's add that to our routes.rb
!FILENAME config/routes.rb
namespace :v1 do
#[...]
resources :performance_data, only: [:create, :index]
end
The next error you will see if you run the spec now, tells you that there is no index method defined
in the controller. Let's create that.
!FILENAME app/controllers/api/v1/performance_data_controller.rb
class Api::V1::PerformanceDataController < ApplicationController
# [...]
def index
@collection = current_api_v1_user.performance_data
render json: { entries: @collection }
end
# [...]
end
We would need to add a few more specs to make sure that the index method works the way it is
intended - to only return PerformanceData that belongs to the current user. But I'll leave that for
you to add.
Note that we are NOT building any views or templates to display data. We MIGHT need more control
of how the json response looks like and for that we will use the template capabilities of JBuilder.
But for now, returning a rather uncomplicated json object will do.
That's it! We don't need to create the update and delete actions. There could be a use-case for
them in the future, but at this stage they are not needed.
219
The Client
The Client
The Client - Step 3
We will be using Ionic to build our mobile client. If you are not familiar with Ionic yet, please go
back in the documentation and complete the Going mobile with Ionic chapter.
The objective of this step is to allow the user to calculate his results. We will NOT be accessing the
API yet. The login functionality will not work once we are finished with this step. That will be the
objective of the next chapter.
Set up
For this app we will be using the sidemenu template.
Run this command it your terminal to create the application.
$ ionic start cooper_client sidemenu
cd into the new folder (cooper_client) and install the npm and bower dependencies.
$ npm install
$ bower install
Note: If you don't have bower installed on your computer you can install it with npm
install -g bower
Once that is ready, you might want to start the Ionic server to see if everything is set up correctly.
$ ionic serve -c --lab
You should see something like this.
220
The Client
The Client
The Client
</div>
</ion-content>
</ion-view>
If you head over to your browser you should see something like this.
223
The Client
This will do for now. We will add the Login functionality further down the road. Right now, we want
to focus on the main functionality of this application - to calculate the test results and give the user
some feedback.
The Client
The Client
#[...]
<ion-item menu-close href="#/app/test">
Cooper Test
</ion-item>
#[...]
Let's focus on the controller. We need to create a new controller in www/js/controllers.js
!FILENAME www/js/controllers.js
#[...]
.controller('TestController', function($scope) {
$scope.data = {};
$scope.calculateCooper = function() {
var person = new Person({
gender: $scope.data.gender,
age: $scope.data.age
});
person.assessCooper($scope.data.distance);
$scope.person = person;
console.log($scope.person)
};
Run the app and input some test data and click Send. You should see the results displayed on the
page.
The Client
The Client
$scope.data = {};
#[...]
};
If you run the application you should see this interface.
228
The Client
229
230
And if you try to send the request again, that should fail.
231
Alright, if that works we should shift our focus to the Ionic application.
We will be using ng_token_auth - a token based authentication module for AngularJS that works
really well with devise_token_auth.
We'll start by installing the library using Bower. Run the install command from your Terminal.
$ bower install ng-token-auth --save
Make sure that angular-cookie, and ng-token-auth are included in your index.html.
!FILENAME www/index.html
<!-- ionic/angularjs js -->
<script src="lib/ionic/js/ionic.bundle.js"></script>
<script src="lib/angular-cookie/angular-cookie.js"></script>
<script src="lib/ng-token-auth/dist/ng-token-auth.js"></script>
Include ng-token-auth in your module's dependencies and add a basic configuration.
!FILENAME www/js/app.js
angular.module('starter', ['ionic', 'starter.controllers', 'ng-token
-auth'])
.constant('API_URL', 'https://ca-cooper-api.herokuapp.com/api/v1'
)
.config(function ($authProvider, API_URL) {
232
$authProvider.configure({
apiUrl: API_URL
});
})
In controllers.js AppCtrl locate the doLogin() method.
!FILENAME www/js/controllers.js
//...
// Perform the login action when the user submits the login form
$scope.doLogin = function () {
$auth.submitLogin($scope.loginData)
.then(function (resp) {
// handle success response
$scope.closeLogin();
})
.catch(function (error) {
// handle error response
$scope.errorMessage = error;
});
};
//...
Now, we need to make some small changes and additions to our view templates.
Change the input field Username to Email and the ng-model from loginData.username to
loginData.email.
!FILENAME www/templates/login.html
//...
<label class="item item-input">
<span class="input-label">Email</span>
<input type="text" ng-model="loginData.email">
</label>
//...
And add a placeholder for display of error messages.
!FILENAME www/templates/login.html
//...
<ion-content>
<div class="row" ng-if="errorMessage">
<p ng-repeat="error in errorMessage.errors">
{{error}}
</p>
</div>
//...
We also want to create a currentUser object. In theAppCtrl add this method to create the
currentUser on successful authentication.
!FILENAME www/js/controllers.js
233
//...
$rootScope.$on('auth:login-success', function(ev, user) {
$scope.currentUser = user;
});
//...
And make use of this object on the about.html template.
!FILENAME www/templates/about/about.html
//...
<div class="col">
Craft Academy Cooper Test Challenge - Mobile Client.
<p ng-if="currentUser">Logged in as {{currentUser.email}}</p>
</div>
//...
One final touch to enhance the user experience. Sometimes the API endpoint will take some time to
respond and the user might be left wondering if his request is actually being processed or not. There is
a very simple way to show the user that we are really processing his request by displaying an overlay
with some sort of a message. In our case "Logging is..." could do, right?
Ionic provides us with $ionicLoading as a way to display such overlays. Let's imlement it.
Add $ionicLoading to the AppCtrl.
!FILENAME www/js/controllers.js
//...
.controller('AppCtrl', function ($rootScope,
$scope,
$ionicModal,
$timeout,
$auth,
$ionicLoading) {
//...
And update the doLogin() function with the following code.
!FILENAME www/js/controllers.js
//...
// Perform the login action when the user submits the login form
$scope.doLogin = function () {
$ionicLoading.show({
template: 'Logging in...'
});
$auth.submitLogin($scope.loginData)
.then(function (resp) {
// handle success response
$ionicLoading.hide();
$scope.closeLogin();
})
.catch(function (error) {
234
$ionicLoading.hide();
$scope.errorMessage = error;
});
};
//...
With this in place an overlay will be displayed while the app is making the request and hidden when
the promise is resolved.
At this stage we have a method to login the user. The next step will be to add an interface to create
and update users. That is, however, something that we leave up to you. Just a friendly reminder, make
sure that you read the ng_token_auth documentation. Everything you need to know is well
documented in the README file of the project.
235
With this factory we will be able to both write to and read from our back-end database.
Let's create a new controller for doing that. Remember that we need to include that factory in our
controller in order to make it accessible. We'll also add two methods, one to save the data and a
second one to retrieve it.
www/js/controllers.js
.controller('PerformanceCtrl', function($scope, performaceData){
$scope.saveData = function(){
};
$scope.retrieveData = function(){
};
})
Let's start with saving the data. On our view where we do the calculations, we want to display a
button that calls the saveData function. But we only want to display that button IF there is a user
logged in and the calculation has been performed.
Modify your test.html template with this code.
www/templates/test/test.html
<div ng-if="person">
<div class="card">
<div class="item item-divider">
Cooper Test results
</div>
<div class="item item-text-wrap">
<p>Person: Age {{person.age}}, Gender {{person.gender}}</p>
<p>Result: {{person.cooperMessage}}</p>
</div>
</div>
<button
ng-if="currentUser"
ng-controller="PerformanceCtrl"
class="button button-full button-calm"
ng-click="saveData(person)">Save results
</button>
</div>
Let's build our saveData function with a success and an error fallback. It can look something like
this for the moment.
www/js/controllers.js
$scope.saveData = function(person){
data = {performace_data: {data: {message: person.cooperMessage}}}
performaceData.save(data, function(response){
console.log(response);
}, function(error){
console.log(error);
237
})
};
If you try this out in the browser while having the console open, you'll see that you'll get an
Unauthorized error.
Well, that's a bit of a problem but we'll solve it. At least we know that the API endpoint is
within our reach. That IS good progress!
So, let's make this work.
The reason we are getting a 401 on the request is because we are not sending any credentials with the
request and thus we can not get authorized.
Let's make sure that we get the necessary info stored in the currentUser object.
At this stage you need to go back to your Rails application for a moment. We need to make an
addition to config/application.rb in order to make the API include authorization credentials
in the response headers.
config/application.rb
# ...
config.middleware.insert_before 0, 'Rack::Cors' do
allow do
origins '*'
#resource '*', headers: :any, methods: [:get, :put, :delete, :po
st, :options]
resource '*',
headers: :any,
methods: [:get, :post, :delete, :put, :options, :head],
expose: %w(access-token expiry token-type uid client),
max_age: 0
end
end
#...
Now we'll be getting the right response from the back-end application. We need to modify the way we
store that information.
Localize the 'auth:login-success' function in our AppCtrl. We will make a change to store
access-token, uid, etc by grabbing that info from the response headers.
www/js/controllers.js
$rootScope.$on('auth:login-success', function (ev, user) {
238
Display data
Before we start retrieving any historical data, let's create a route and a view template for showcasing
it.
239
$scope.retrieveData = function(){
$ionicLoading.show({
template: 'Retrieving data...'
});
performaceData.query({}, function(response){
$state.go('app.data', {savedDataCollection: response.entries});
$ionicLoading.hide();
}, function(error){
$ionicLoading.hide();
$scope.showAlert('Failure', error.statusText);
})
};
//...
We also need to create a new controller to handle the view. When the view is entered, we want to
retrieve the data sent in params and save it in the current $scope.
www/js/controllers.js
//...
.controller('DataCtrl', function($scope, $stateParams){
$scope.$on('$ionicView.enter', function () {
$scope.savedDataCollection = $stateParams.savedDataCollection;
});
})
//...
Finally, we can update our template and display the data. The way we do that is to iterate through the
array of entries and condition the display IF there is a message key in the data attribute (remember
the way we store the results in our database?).
We also need to format the date - please read the docs for date in AngularJS.
www/templates/test/data.html
<ion-view title="Historical Data">
<ion-content>
<div ng-if="savedDataCollection" ng-repeat="entry in savedDataCo
llection ">
<p ng-if="entry.data.message">{{entry.data.message}} - {{entry
.created_at | date:'mediumDate'}}</p>
</div>
</ion-content>
</ion-view>
That will do it for now. The next challenge is to present the data as charts. That can be
interesting...
241
Display charts
Display charts
Display charts - step 6
We will use Angular Chart to display users historical data.
$ bower install chart.js#2.3.0-rc.1 --save
$ bower install angular-chart.js --save
As always, make sure to include the library in your intex.html.
!FILENAME www/index.html
<!-- ionic/angularjs js -->
//...
<script src="lib/chart.js/dist/Chart.min.js"></script>
<script src="lib/angular-chart.js/dist/angular-chart.min.js"></script
>
And also, make chart.js available to your app by adding it to the main module.
!FILENAME www/js/app.js
angular.module('starter', ['ionic', 'starter.controllers', 'starter.
services', 'ng-token-auth', 'ngResource', 'chart.js'])
Okay, here comes the tricky part. We want to display two charts on our view. One Doughnut Chart
and one Radar Chart. The tricky part is that we only want to display labels for values that are actually
stored in the collection of historical data. Meaning for instance that if a user has stored several
"Average" and "Above Average" entries, then we should only show those two labels with a value.
Nothing else. Same thing goes for both chart types.
So what we need to do is to go through the savedDataCollection and get unique values from
data.message and store them in an array. This is the responsibility of the following function.
function getLabels(collection) {
var uniqueLabels = [];
for (i = 0; i < collection.length; i++) {
if (collection[i].data.message && uniqueLabels.indexOf(collectio
n[i].data.message) === -1) {
uniqueLabels.push(collection[i].data.message);
}
}
return uniqueLabels;
}
We are going to store that array in $scope.labels.
The second thing we need to do is to get the value of how many times each message is present in the
collection. For that we add another function that we use when iterating over $scope.labels and
store the results in $scope.data
242
Display charts
angular.forEach($scope.labels, function(label){
$scope.data.push(getCount($scope.savedDataCollection, label));
});
function getCount(arr, value){
var count = 0;
angular.forEach(arr, function(entry){
count += entry.data.message == value ? 1 : 0;
});
return count;
}
All in all, the DataCtrl should look something like this.
!FILENAME
.controller('DataCtrl', function ($scope, $stateParams) {
$scope.$on('$ionicView.enter', function () {
$scope.savedDataCollection = $stateParams.savedDataCollection;
$scope.labels = getLabels($scope.savedDataCollection);
$scope.data = [];
angular.forEach($scope.labels, function(label){
$scope.data.push(getCount($scope.savedDataCollection, label));
});
$scope.radardata = [$scope.data];
});
function getLabels(collection) {
var uniqueLabels = [];
for (i = 0; i < collection.length; i++) {
if (collection[i].data.message && uniqueLabels.indexOf(collect
ion[i].data.message) === -1) {
uniqueLabels.push(collection[i].data.message);
}
}
return uniqueLabels;
}
function getCount(arr, value){
var count = 0;
angular.forEach(arr, function(entry){
count += entry.data.message == value ? 1 : 0;
});
return count;
}
})
Finally, let's turn out attention to the view template.
We want to add markup for the two charts and clear up the data display.
!FILENAME
243
Display charts
244
Display charts
245
Wrapping up
Wrapping up
Wrapping up
What we have focused on is the most basic functions and connecting the client to the back-end api. As
with all walk-throughs in this book, there's plenty of functionality you can add to this app no make the
user experience much better.
Some suggestions as to what to do next:
More user management functions
Sign out
Register
Update account, etc
OAuth login (Facebook, Twitter)
UI enhancements
Colors
Backgrounds
Display current user profile name in side menu
Add mote chart types
Functionality
Add more the the Cooper Test
Calculate VO2-max
Add the BMI calculator
Add local storage for better performance (ni internet connection)
Resources
Generate icons and a splash screen.
There's plenty of possibilities!
246
Results tables
Results tables
Results tables
Required Resources
To undertake this test you will require:
400 meter track
Stopwatch
Whistle
Assistant
How to conduct the test
This test requires the athlete to run as far as possible in 12 minutes.
The athlete warms up for 10 minutes
The assistant gives the command GO, starts the stopwatch and the athlete commences the test
The assistant keeps the athlete informed of the remaining time at the end of each lap (400m)
The assistant blows the whistle when the 12 minutes has elapsed and records the distance the
athlete covered to the nearest 10 meters
Assessment
The following normative data is available for this test:
Males
Age Excellent Above Average
13-14 >2700m
15-16 >2800m
17-19 >3000m
20-29 >2800m
30-39 >2700m
40-49 >2500m
50+ >2400m
2400-2699m
2500-2799m
2700-2999m
2400-2799m
2300-2699m
2100-2499m
2000-2399m
Average
2200-2399m 2100-2199m
2300-2499m 2200-2299m
2500-2699m 2300-2499m
2200-2399m 1600-2199m
1900-2299m 1500-1999m
1700-2099m 1400-1699m
1600-1999m 1300-1599m
<2100m
<2200m
<2300m
<1600m
<1500m
<1400m
<1300m
Females
Age Excellent Above Average
13-14 >2000m
15-16 >2100m
17-19 >2300m
20-29 >2700m
30-39 >2500m
40-49 >2300m
50+ >2200m
247
1900-1999m
2000-2099m
2100-2299m
2200-2699m
2000-2499m
1900-2299m
1700-2199m
Average
1600-1899m 1500-1599m
1700-1999m 1600-1699m
1800-2099m 1700-1799m
1800-2199m 1500-1799m
1700-1999m 1400-1699m
1500-1899m 1200-1499m
1400-1699m 1100-1399m
<1500m
<1600m
<1700m
<1500m
<1400m
<1200m
<1100m
Results tables
248
Learning Objectives
Learn about Design Sprint
249
Main features
Main features
Main features
Core functionality
Allow local restaurant operators to publish their menus and accept orders.
Allow the general public to browse local restaurants and place orders for home delivery
Location
We want the system to be used in several geographical locations. All Restaurants in the system should
be geo-coded by street address. All users should be geo-coded using their browser location or/and
their IP address
Categories (restaurant)
Each restaurant in the system should belong to a category (i. e. Thai, Chinese, French, Italian, etc).
Restaurants
We want to be able to host several Restaurants in each geographical location.
A restaurant should specify what radius it will deliver to (5, 10, 20, 30 km).
Rating
Every Restaurant should be rated using rating system (1 - 5) with a possibility to add an optional
comment.
Delivery
All orders from restaurants within a 3 kilometer radius should be able to be delivered together. Orders
from restaurant in a wider area will add an extra delivery fee. The system should be able to handle
free delivery as fell as charged.
Menus
Each restaurant should have one or more menus (lunch, la carte, etc.). Each menu contains several
dishes grouped in configurable categories.
Categories (dish)
The system should have some default categories set up (Starter, Main course, Dessert) but also allow
System users and Administrators to define their own.
Dishes
250
Main features
Each dish should be described and have some optional fields containing allergy information,
ingredients, calories, etc. If ingredients are active for a dish then the user should be able to choose to
if he want to remove a specific ingredient.
Order
An order should take into account all dishes from several restaurants in the area.
User roles
Administrator - Can perform all CRUD actions on all objects in the system
System user - Can create restaurants with associated objects (see below)
User - Can place orders and add rating to Restaurants
UI
Index/Landing page
The index page of the system should list restaurants in the proximity of the user that visits the system.
The page should display a map and ask the user a question "What do you feel like tonight?" The user
should be given a set of restaurant categories to choose from. Once a choice has been made, the
restaurants should pop up as markers on a map. A separate list (with links) of restaurants should be
displayed underneath the map. The list should include the user rating average of the restaurant.
The restaurant show page
Clicking on a marker or a restaurant name/description in the list should take the user to the restaurants
show page. Apart from the restaurant name, description and address, the page should also include a
list of popular/selected dishes with ability to order ("add to order" button).
A link to the full menu should be placed on the page. (i.e. "see full menu")
The menu page
The menu page should show all dishes with pictures and a brief description. A link to full dish show
page should be visible on the list.
The dish show page
The dish show page should show a detailed view of the dish with full description and optional
information (activated by the System user that owns the Restaurant object) about the dish.
251
Design Sprint
Design Sprint
Design Sprint
As a first step your team needs to gather for a working session called the Design Sprint. In real
projects this is done over several days (2 - 5 days approximately). In this simulation we will set aside
one day to this task.
Normally, this sprint includes a lot of input from the Client (Product Owner) and relies on his
participation. We will rely in the information provided in the previous chapter and any questions we
have will have to be answered by the team or, in the uncertainty is to high, just ignored.
The main objective of this workshop/session is create a Backlog of feature requests that are described
in a structured way, discuss each feature, define the definition of done, and assess the complexity of
the implementation.
Epics
The first step is to create a series of user stories for the main functions outlined in the system
requirements. Initially these will be on a relatively high level and be easily mapped to the overall
business objectives of the system - we will refer to them as Epics.
Acceptance Criteria
Once we have defined a set of features and chores for each epic, it is time to start thinking about the
definition of done. We create a list of Acceptance Criteria for each entry in our backlog. This list is
crucial for setting a scope for the feature and defining when the feature should be considered done and
delivered. (In real projects this criteria are agreed upon with the client. In our simulation, we will be
responsible for setting them within the team.)
Complexity
Once all features and chores has been grouped into epics and the acceptance criteria for each one of
them has been defined, it's time to review all of them and talk about how complex and resource
consuming the implementation of each story will be.
We will use a fairly straight forward way of assessing complexity - making use of a three point scale.
A story that is deemed pretty simple and will only take a few hours to both implement and test should
be given 1 complexity point.
A story that will require more than a day to develop, test and ship, or a feature that relies on a third
party API or service, should be approach with caution, deemed to be of intermediate complexity and
given 2 complexity points.
252
Design Sprint
A story that will require implementation through the entire stack (new models with relation,
controllers and views) and features that relies on many conditions that needs to be met, should be
considered complex and given 3 points.
Note that during this process you might find yourselves in a situation where the complexity of a
specific feature or a chore won't fit in the three point scale. In this case you need to split the story into
two or more separate features or chores and assess them individually.
253
Pivotal Tracker
Pivotal Tracker
Pivotal Tracker
In this project, we will be using Pivotal Tracker as a tool for planning and collaboration.
Pivotal Tracker is a straightforward project planning tool that allows teams to collaborate and react
instantly to real-world changes. Its based on agile software development methods, but it can be used
on a variety of projects. Tracker frees you up to focus on getting things done, without getting bogged
down, keeping your plans in sync with reality.
254
Pivotal Tracker
1. Sign up for an unlimited 30-Day Free Trial (no credit card required) or click the link in a
project invitation email to set your password and sign in.
2. Click Create Project on the Dashboard. Enter a name to create a project.
The Basics
The core unit of projects is called a story. Stories will usually be one-line descriptions of features that
should be implemented in the project. The description can be further expanded in the story's details.
Stories are arranged by their priority the most important ones will be on the top of the list. They
are also divided into a few panels based on their status. The Backlog contains all of the project's
stories.
From left to right there are three panels: Current, Backlog and Icebox.
Current
The Current panel is the list of stories the team is working on this week.
Backlog
The Backlog contains all the stories that the Product Owner has prioritized for the team to do next.
Ideally, the stories at the top of the backlog have already been estimated as well. This is where the
team looks for additional stories to work on once they've finished what's in the current pile.
Based on the velocity of the project (how quickly features are implemented and approved), the most
255
Pivotal Tracker
important ones will be moved to the Current section so you have easy access to them.
Icebox
The Icebox is where you collect all your stories. Whether your starting a project or are adding new
stories to an ongoing project this is where all stories that has not been prioritized live. In real projects,
it is up to the customer to maintain the icebox and feed stories from it to the backlog. In our
simulation
Project view
We have used Pivotal Tracker for many projects within Agile Ventures. Here is an example of a
dashboard for Project Unify. It is a public project and you can visit that dashboard on
https://www.pivotaltracker.com/n/projects/1525675
256
Pivotal Tracker
Feature view
Pivotal Tracker allows you to describe each feature, chore or bug in a very detailed way and fully
supports the agile methods we use in our project simulations. Here is an example of a feature with a
user story, acceptance criteria, complexity points, etc.
257
Pivotal Tracker
Remember, the tool you use can support your workflow but it won't do the job for you. You
need to add the content and work with maintaining it actively.
258
Cloud Storage
All image assets needs to be stored on an AWS S3 bucket.
Action points
Create stories in PT regarding Cloud Storage functionality
Create an account on AWS
Create an S3 bucket
Research an appropriate gem to use
Set up necessary permissions to upload assets to the S3 bucket from your application
RESTful API
All application data should be able to be retrieved through a separate set of uri's. Authorized users
should be able to place orders using the API.
Action points
Create stories in PT regarding API functionality desribing all endpoints you deem necessary
Create a new namespace in your application
Set up tests (request specs) for the API endpoints
Set up controllers for various API endpoints
If necessary, add custom views using JBuilder
The API should be accessible using Postman.
259
Extras
Extras
Extras
260
Naming Standards
Naming Standards
Naming standards
You are free to set your own class and methods names.
Methods
When naming methods the goal is to be descriptive but short. Name based on what it will return or
what the major intended side effect will be. You shouldn't be missing any parts from the name
because the method should only do one thing anyway. If you can't tell what the method will return
based on the name, you probably need a better name. If your method name seems insanely long, your
method may be trying to do more than one thing. End with a question mark ? if it will return
true/false.
Method names should begin with a lowercase letter (or an underscore). Apart from letters, only ?, !
and = characters are allowed as method name suffixes. This is a list of suggestions on naming
standards for different kind of methods:
Methods that do something should be verbs:
obj.calculate
obj.set_name
obj.get_date
Methods that are accessors (or behave like them) should be nouns:
foo = obj.name
obj.date
obj.order_total
Interrogative methods (if the method returns true/false) get phrased as questions:
obj.date_today?
obj.name?
obj.calculation_done?
Methods that modify the object itself, should be exclamations:
obj.truncate_body!
obj.remove_name!
Variables
Variables in Ruby can contain data of any type. You can use variables in your Ruby programs without
any declarations. Variable name itself denotes its scope (local, global, instance, class.).
A local variable (declared within an object) name consists of a lowercase letter (or an
underscore) followed by name characters (balance, cyrrent_collection).
An instance variable name starts with an @ sign followed by a name (@sign, @name, etc).
A class variable name start with a double @ sign (@@) and may be followed by digits,
underscores, and letters, e.g. @@colour
261
Naming Standards
Global variables starts with a dollar ($) sign followed by other characters, e.g. $global
262
Classes vs Modules
Classes vs Modules
Classes and Modules
Classes are about objects; Modules are about functions.
Modules are about providing methods that you can use across multiple classes - think about them as
"libraries". A Module is just a collection of methods and constants and comes in handy at times when
you want to group things together that don't naturally form a Class. Classes are also a collection of
methods and constants, but with the added functionality of being able to be instantiated. A Module
cannot be instantiated.
Class
Instantiation can be instantiated
Usage
object creation
Superclass Module
class methods and
Methods
instance methods
can inherit behaviour and
Inheritance can be base for
inheritance
Inclusion
Extension
Module
can not be instantiated
mixin facility, provide a namespace.
Class
module methods and instance methods
no inheritance
Class - methods
Class methods are methods that are called on a class and instance methods are methods that are called
on an instance of a class.
class FooClass
def self.bar
'class method'
end
def baz
'instance method'
end
end
FooClass.bar # => "class method"
FooClass.baz # => NoMethodError: undefined method baz for Foo:Class
Classes vs Modules
o:0x1e820>
Module - methods
The methods in a module may be instance methods or module methods. You can include methods in
your module that you can be both functions or included by one or several Classes and used as instance
methods. Instance methods appear as methods in a class when the module is included, module
methods do not. Conversely, module methods may be called without creating an encapsulating object,
while instance methods may not.
There are several ways you can create methods in a Module. Let's say you want to be able to do
FooModule.foo(some_value)
You can define the foo method like this
module FooModule
def self.foo(bar)
bar
end
end
Or like this...
module FooModule
extend self
def foo(bar)
bar
end
end
...or like this.
module FooModule
module_function
def foo(bar)
bar
end
end
264
Code structure
Code structure
Code structure
Great code means great class and method names. Great code means also a great structure.
265
Bower
Bower
Bower
Bower is a front-end package manager built by Twitter. As a package manager, Bower simplifies
installing and updating project dependencies, which are libraries you use in your project.
Browsing all the library websites, downloading and unpacking the archives, copying files into the
project folder all of this can be replaced with a few Bower commands in the terminal.
266
Bower
Installing Bower
Bower can be installed using npm, the Node package manager (If you don't already have npm
installed, head over to the Node.js website and follow the install instructions). The npm program is
included in Node.js.
Once you have npm installed, open up your terminal and run the following command:
$ npm install -g bower
This will install Bower globally on your system.
Now that you have Bower installed, we can start looking at the commands that are used to manage
packages.
Finding Packages
There are two different ways that you can find Bower packages. Either using the online component
directory, or using the command line utility.
To search for packages on the command line you use the search command. This should be followed
267
Bower
Installing packages
To add a new Bower package to your project you use the install command. This should be passed the
name of the package you wish to install.
$ bower install [package]
As well as using the package name, you can also install a package by specifying one of the following:
A Git endpoint such as git://github.com/components/jquery.git
A path to a local Git repository.
A shorthand endpoint like components/jquery. Bower will assume that GitHub is being used, in
which case, the endpoint is the part after github.com in the repository URL.
A URL to an archive (a zip or tar file). The files in the archive will be extracted
automatically.
You can install a specific version of the package by adding a pound-sign (#) after the package name,
followed by the version number.
$ bower install [package]#[version]
Installed packages will be placed in a bower_components directory. This is created in the folder
which the bower program was executed. You can change this destination using the configuration
options in the .bowerrc file.
268
Bower
Once installed, you can use a package by simply adding a <script> or <link> tag to your HTML
markup. Although Bower packages most commonly contain JavaScript files, they can also contain
CSS or even images.
<script src="path/to/bower_components/jquery/jquery.min.js"></script
>
Bower
Updating Packages
Updating a package is pretty simple. If you've used a bower.json file you can run a simple update
command to update all of the packages at once. However, the update tool will abide by the version
restrictions you've specified in the bower.json file.
$ bower update
To update an individual package you again use the update command, this time specifying the name of
the package you wish to update.
$ bower update [package]
Uninstalling Packages
To remove a package you can use the uninstall command followed by the name of the package
you wish to remove.
$ bower uninstall [package]
It's possible to remove multiple packages at once by listing the package names.
bower uninstall Chart.js ng-token-auth angular-resource
Bower
After reading terms like CSS, Javascript and HTML, you are already having nightmares thinking at
the idea of bringing a 3rd party tool into the dreaded asset pipeline but fear not as Bower and the asset
pipeline actually play very well together. Without further ado, here's how to set the whole thing up.
Install Bower
Make sure you have bower installed (see above)
Configuring Bower
By default Bower install the components (what it calls JS/CSS libraries) in bower_components
folder, which doesn't really play with the standard Rails folder hierarchy. To change that, simply
create a .bowerrc (bower's config file) file at the root of your Rails app and add this:
{ "directory": "vendor/assets/bower_components" } This will tell Bower to save all of the component
files in that directory which follow Rails' convention on storing assets.
package.json
From the root of your Rails app, run bower init to create your bower.json file.
Depending on your answers, your bower.json should look something like this:
{
name: 'MyApp',
version: '0.0.1',
authors: [ ],
description: 'Your description',
license: 'MIT',
homepage: 'http://whatever.com',
ignore: [
'**/.*',
'node_modules',
'bower_components',
'test',
'tests'
],
"dependencies": {
"angular": "~1.2.16"
}
}
Now if I run bower installin your terminal, it will pull Angular 1.2.16 from the Github repo into
vendor/assets/bower_components.
Configuring Rails
Now that Bower is installed and working, you need to make sure that Rails play nice with it.
To do so, head to your config/application.rb file to let the asset pipeline about it. Simple
add the following line to your file and save it.
config.assets.paths << Rails.root.join('vendor', 'assets', 'componen
271
Bower
ts')
Including assets
To require the components downloaded using Bower, open up
app/assets/javascripts/application.js and require the necessary JS file like you
would usually do. In the example with Angular, you do so by adding this one line to it.
//= require angular/angular
For CSS frameworks and libraries like Bootstrap or Foundation, you can pop in the following line to
app/assets/css/application.css.
*= require bootstrap/dist/css/bootstrap
That's it! You can now use Bower to manage all of the 3rd party CSS & Javascript libraries you want
to use in your project and Rails' asset pipeline will take care of the rest for you.
272
The README
It is important to ensure that the projects README includes correct and relevant information that will
allow a new developer to get the necessary insights about your project in order to use and understand
the application.
In our code reviews we want to pay attention to a well written and structured README. Please
follow this guide for how to structure a good README while performing your code review.
The Setup
Checkout the code follow the setup instructions. What you want to be looking for is:
Are the setup instructions in the README correct?
Are there any pitfalls in the set up process?
The Tests
Once you are set up with the application, run all automated tests.
What you want to be looking for is:
Are the tests passing after a vanilla install?
Is there any test coverage metrics? How much of the code is covered in tests?
The Functionality
Read the code and try and use the app through the web interface if there is one, or load the code in the
console (IRB or Rails console) and experiment with it.
What you want to be looking for is:
Is the application behaving the way it is intended?
Are there any outstanding functions that are still not been implemented?
The Code
Now, go over the code and ...
https://github.com/eliotsykes/rails-code-review
https://github.com/CraftAcademy/airport_challenge/blob/master/docs/review.md
273
274
About README's
About README's
About README's
It is important to ensure that the projects README includes correct and relevant information that will
allow a new developer to get the necessary insights about your project in order to use and understand
the application.
With a comprehensive, well-written README any developer should be able to hop on to your project
and begin writing code within 10 minutes. If you consider that over the course of developing an app
you'll likely see multiple developers set up the app multiple times, you'll cumulatively save hours of
developer time with just minutes of work.
General Information
The General Information section should give a new developer an idea of what the project is about
and who is involved with it.
Information you might want to include is:
The name of the project
The name and contact details of the client and any 3rd party vendors as well as names of the
developers on the project
A brief description of the project that provides the answer to the most important question of
them all: What problem is this project solving?
An outline of the technologies in the project.programming language, frameworks, important
gems, database, ORM, etc.
Links to any related projects
Links to online tools related to the application (e.g.: Links to the CI server, Pivotal Tracker or
Waffle board, development and staging servers, the production site, etc.
Getting Started
The Getting Started section outlines the process of getting the app installed for a new developer.
Information you might want to include is:
A detailed spin-up process. This should include:
Instructions on installing any software and/or gems the application is dependent on.
Detailed instructions on running the app (with step-by-step commands needed to get up
and running on a local machine)
A list of settings that need to be configured (for third party API's)
A list of seeded credentials that can be used to log in with each user type in the system
Any information about subdomains in the app (e.g.: api.myapp.dev/)
A good tip is to When writing instructions pretend youre writing them for someone who
knows next to nothing about developing in the framework/language your application
uses.
Testing
275
About README's
The Testing section need to include the commands to run any of the test suites you have (RSpec,
Jasmine, Cucumber) and any setup you need to do before-hand
Deployment
The Deployment section should include any information needed to deploy the application to a remote
server.
Note that README files are most commonly written in Markdown - a lightweight and easy-touse syntax to style text on the web. Mastering Markdown is easy but do give this Guide a read.
276
MVC
MVC
Model View Controller
Model View Controller (MVC) is a 3-tiered architectural software design pattern for
implementing user interfaces. It divides a given software application into three interconnected parts,
to separate internal representations of information from the ways that information is presented to or
accepted from the user.
In addition to dividing the application into three kinds of components, the MVC design pattern
defines the interactions between them.
A Model stores data that is retrieved according to commands from the Controller and
displayed in the View.
A View generates new output to the user based on changes in the Model.
A Controller can send commands to the model to update the Model's state. It can also send
commands to its associated View to change the it's presentation of the Model.
MVC
Alright, so you already know the basics of the Request response pattern - the MVC pattern deals with
what is happening with the request by your web- and application server and the application itself. At
the and, an appropotiate responce is sent back to the client (the browser or whatever has made the
request) and forgotten about.
Instead of a sterile 3-tiered architecture walkthrough, it's more fun to imagine a story with fat
model, skinny controller, dumb view .
Models do the grunt work, Views are the happy face, and Controllers are the masterminds behind it
all.
Think about this flow:
The browser makes a request (such as http://localhost:9292/dish/15)
The web server (WEBrick, etc.) receives the request. It uses routes to find out which
controller to use. The web server then uses the dispatcher to create a new controller, call the
action and pass the parameters.
The Controller do the work of parsing user requests (data submissions, cookies, sessions and
other browser stuff). They're the pointy-haired manager that orders employees around. The
best controller is Dilbertesque: It gives orders without knowing (or caring) how it gets done.
Models are Ruby classes extended by ORM methods. They talk to the database, store and
validate data, perform the business logic and otherwise do the heavy lifting. They're the chubby
guy in the back room crunching the numbers.
Views are what the user sees: HTML, CSS, JavaScript, JSON. They're the sales rep putting up
flyers and collecting surveys, at the managers direction. Views are merely puppets reading what
the controller gives them. They don't know what happens in the back room.
Finally, the Controller returns the response body (HTML, XML, etc.) and metadata (caching
headers, redirects) to the server. The server combines the raw data into a proper HTTP
response and sends it to the client to be presented to the user.
The web server is the invisible gateway, shuttling data back and forth (users never interact with
the controller directly).
278
Three-Tier Architecture
Three-Tier Architecture
Three-Tier Architecture
Three-tier architecture is an architectural deployment style that describe the separation of
functionality into layers with each segment being a tier that can be located on a physically separate
computer.
Three-tier application architecture is characterized by the functional decomposition of applications,
service components, and their distributed deployment, providing improved scalability, availability,
manageability, and resource utilization.
Structure
Using this architecture the software is divided into 3 different tiers: Presentation, Logic (also
refereed to as "business logic", "data access tier", or "middle tier"), and Data. Each tier is developed
and maintained as an independent tier.
Presentation tier
This is the topmost level of the application. The presentation layer provides the application's user
interface (UI). This communicates with other tiers by outputting results to the browser/client tier and
all other tiers in the network.
Logic tier
The Logic tier controls an application's functionality by performing detailed processing. Logic tier is
where mission-critical business problems are solved.
Data tier
Here information is stored and retrieved. This tier keeps data neutral and independent from
application servers or business logic. Giving data its own tier also improves scaleability and
performance.
Benefits
Maintainability - Since each tier is independent of the other tiers, updates or changes can be
carried out without affecting the application as a whole.
Scalability - Because tiers are based on the deployment of layers, scaling out an application is
reasonably straightforward.
Flexibility - Because each tier can be managed or scaled independently, flexibility is increased.
Availability - Applications can exploit the modular architecture of enabling systems using
easily scalable components, which increases availability.
279
AngularJS
AngularJS
AngularJS
A basic application
$ bower init
Install Angular
$ bower install angularjs --save
Create index.html
<!DOCTYPE html>
<html lang="en-US">
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.4.8/an
gular.min.js"></script>
<body ng-app="">
<div >
<p>Name : <input type="text" ng-model="name"></p>
<h1>Hello {{name}}</h1>
</div>
</body>
</html>
280