Skip to content
Permalink
Branch: master
Find file Copy path
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
683 lines (503 sloc) 20.7 KB
---
title: "Lesson 3: Ruby on the Web"
weight: 50
---
# Lesson 3: Ruby on the Web
## Introduction
### Objectives
In this tutorial we are going to look at:
* using `require` to access other Ruby modules
* using Ruby's `Date` type
* using `File` to read files in Ruby
* what HTTP is and how it works
* using Google Chrome's Developer tools to inspect HTTP requests
* building a web server with Ruby
* templating HTML
* introducing web security
### Goal
By the end of this tutorial you will have built [this web application](https://apply-for-a-barking-permit-ruby.london.cloudapps.digital/home):
<a href="https://apply-for-a-barking-permit-ruby.london.cloudapps.digital/home">
<img src="images/lesson-3/finished-application.png" alt="Apply for a Barking Permit website, showing the form from lesson 1 and the confirmation page">
</a>
You'll have a basic understanding of how web applications work, and a good foundation to build on.
### Using `require` to access other Ruby modules
In lesson 2 we used a number of built-in Ruby methods, like `rand` (to get
a random number), `.reverse` (to reverse a string), `gets` and
`puts` (to read and write strings from the command line).
These methods are so useful that they live in the core of Ruby, so you can
always call them.
Some bits of Ruby are used less often, and these are not included by default.
To use these other bits you need to `require` them. For example, the `"date"`
module lets you work with dates and times, but it isn't included by default.
Running `require("date")` includes the date code so you can use it in your
script. For example:
```
irb(main):001:0> require("date")
=> true
irb(main):002:0> Date.today
=> #<Date: 2019-05-30 ((2458633j,0s,0n),+0s,2299161j)>
```
### Working with Dates in Ruby
Ruby's date type makes doing things like adding days to a date much easier
(if you didn't have a date type you'd need to worry about how many days there
are in a month and so on).
```
irb(main):003:0> one_thousand_days_in_the_future = Date.today + 1000
=> #<Date: 2022-02-23 ((2459633j,0s,0n),+0s,2299161j)>
```
The `.today` method gets today's date, but you can also create dates for
other days using `.new`:
```
irb(main):004:0> Date.new(2022, 2, 23)
=> #<Date: 2022-02-23 ((2459633j,0s,0n),+0s,2299161j)>
```
Because different cultures represent dates in different ways (e.g. Japan use
2019-05-30, the USA use 05/30/2019 and the UK use 30/05/2019), there's a
special method for formatting dates as strings called `.strftime`. It takes
a "format" string as an argument specifying how to show the date:
```
irb(main):005:0> japan_format = Date.today.strftime("%Y-%m-%d")
=> "2019-05-30"
irb(main):006:0> usa_format = Date.today.strftime("%m/%d/%Y")
=> "05/30/2019"
irb(main):007:0> uk_format = Date.today.strftime("%d/%m/%Y")
=> "30/05/2019"
```
There are lots of placeholders you can use in your date string, including:
* `%Y` - the year as a 4 digit number
* `%m` - the month as a 2 digit number
* `%d` - the day as a 2 digit number
* `%A` - the day of the week as a word (e.g. Thursday)
#### Task 1: What day were you born on?
Using `irb` and the `Date` type, work out what day of the week you were born on.
<details>
<summary>Answer</summary>
```
irb(main):001:0> require("date")
=> true
irb(main):002:0> birthday = Date.new(1988, 1, 20)
=> #<Date: 1988-01-20 ((2447181j,0s,0n),+0s,2299161j)>
irb(main):003:0> birthday.strftime("%A")
=> "Wednesday"
```
</details>
### Using `File` to read files in Ruby
Sometimes it's nice to be able to store things in files, and read these files
using Ruby so we can do something with the contents. Ruby has a `File.read`
method that lets us do this.
`File.read` takes the path to the file as an argument, and returns the
contents of the file as a string.
```
irb(main):004:0> File.read("/Users/your-username/Desktop/lesson-1-html-and-css/index.html")
=> "<!doctype html>\n<html>...
```
We can work with the contents using all the string methods (like
`.reverse`) we've already seen.
There are lots of other useful methods like `.size` (which returns how many
characters there are in the string), and `.sub` (which takes two arguments,
and substitutes the first occurence of the first string with the second).
```
irb(main):005:0> "Cats are the best".sub("Cat", "Dog")
=> "Dogs are the best"
irb(main):006:0> "Dogs are the best".size
=> 17
```
#### Task 2: count the characters in a file
We created a file in `lesson-1-html-and-css/index.html`. Use `File.read`
and `.size` to work out how many characters there are in the file.
> In Finder you can copy the path to a directory by right clicking, holding
down the option key (`⌥`), and choosing *Copy "Directory" as Pathname*.
Alternatively, you can drag the folder onto the terminal and it will type
the path for you.
<details>
<summary>Answer</summary>
```
irb(main):006:0> file = File.read("/Users/your-username/Desktop/lesson-1-html-and-css/index.html")
=> "<!doctype html>\n<html>...
irb(main):007:0> file.size
1260
```
</details>
## Introduction to HTTP
### What is HTTP?
HTTP stands for <strong>H</strong>yper<strong>T</strong>ext <strong>T</strong>ransfer <strong>P</strong>rotocol.
HTTP describes the format of the communications between your web browser and
a "web server" (which is just a kind of computer).
Your browser sends **HTTP requests** and the server responds with **HTTP responses**.
For example, when you visit `www.gov.uk` your browser will send a request a like this:
```http
GET / HTTP/1.1
Host: www.gov.uk
```
And GOV.UK's server will respond with something like:
```
HTTP/1.1 200 OK
Date: Wed, 29 May 2019 12:08:17 GMT
Content-Type: text/html; charset=UTF-8
Content-Length: 32897
<!doctype html>
<html>
... snip ...
</html>
```
In this case the response contains the HTML of the GOV.UK homepage.
### Viewing HTTP requests using browser developer tools
Modern browsers come with built in tools that let you see what's happening
under the hood. In Google Chrome on a Mac you can bring these up for any
webpage by pressing Command + Option + I (`⌘ + ⌥ + I`).
![Screenshot showing the Google Chrome developer tools on www.gov.uk](/images/lesson-3/developer-tools.png)
The Network tab shows you the requests and responses your browser is sending and receiving.
#### Task 3: Use the developer tools to look at an HTTP request
Go to any website you like and use the developer tools to look at the first
HTTP request it makes.
<details>
<summary>Answer (screenshots)</summary>
![Screenshot showing the Google Chrome developer tools network tab](/images/lesson-3/developer-tools-network.png)
![Screenshot showing the Google Chrome developer tools focused on a particular request](/images/lesson-3/developer-tools-network-focused.png)
</details>
## Building web servers with Ruby
A web server is a computer program that can understand HTTP requests and
respond to them with HTTP responses.
In lesson 1 we built a website which sent the same HTML back for every
request. By getting Ruby involved in handling the request we can make our
website dynamic (so the response is not always the same). We can also perform
side effects in response to certain requests (like sending emails, or
printing out physical barking permits).
Ruby is a great language to get started with, because it has everything you
need to build simple web servers built right in to the language.
First of all, let's create some files for us to serve.
#### Task 4: Create files and directories
* Create a folder called `lesson-3-ruby-on-the-web` next to your other lesson folders.
* Inside this folder, create another folder called `public`, and copy the images folder from lesson 1 inside.
You should end up with a directory structure like:
```
lesson-3-ruby-on-the-web
└── public
└── images
├── puppy-in-a-hat.jpg
└── shiba-inu.jpg
```
### Hello WEBrick!
Earlier we `require`'d `"date"` so we could do some things with dates which
aren't in the core ruby language. Similarly, we can require a thing called
`"webrick"` which allows us to build HTTP servers.
We can use WEBrick to create a new server by calling the
`WEBrick::HTTPServer.new` method like this:
```ruby
require("webrick")
server = WEBrick::HTTPServer.new(Port: 8080, DocumentRoot: './public')
```
This might look a bit confusing because it uses some bits of Ruby we haven't
introduced yet (what are all these colons doing all of a sudden?). Don't
worry about it - at some point you'll learn about modules, classes, and named
arguments and it will all make sense.
The `server` lets us respond to HTTP requests in any way we like. We can use
the `.mount_proc` method to attach a block of Ruby code to a particular request like this:
```ruby
server.mount_proc("/home") do |request, response|
puts("Hello server!")
response.body = "Hello browser!"
end
```
`.mount_proc` is a bit like `.times` from lesson 2:
```ruby
10.times do |i|
puts("#{10 - i} green bottles, hanging on the wall,")
# ...
end
```
only instead of running the block 10 times straight away, WEBrick will run
the block every time a request is made to the `/home` path.
The final thing we need to do to make the server work is start it, which we
do by calling the `.start` method after we've made all our calls to
`.mount_proc`:
```ruby
server.start
```
#### Task 5: Create a "hello world" server
* Create a file inside the `lesson-3-ruby-on-the-web` folder called `hello-world-server.rb`
* Copy / paste (or type) the following ruby into `hello-world-server.rb`
```ruby
require("webrick")
server = WEBrick::HTTPServer.new(Port: 8080, DocumentRoot: './public')
server.mount_proc("/home") do |request, response|
puts("Hello server!")
response.body = "Hello browser!"
end
server.start
```
* On the command line, change directory into `lesson-3-ruby-on-the-web` using `cd`
* Run `hello-world-server.rb` from the command line with `ruby hello-world-server.rb`
* Visit [http://localhost:8080/home](http://localhost:8080/home) in your browser
* Reload the page a few times to see what happens
<details>
<summary>Answer</summary>
You should see a web page that says "Hello browser!" and you should see some lines on the command line like this:
```
[2019-05-29 21:38:46] INFO WEBrick 1.3.1
[2019-05-29 21:38:46] INFO ruby 2.3.7 (2018-03-28) [universal.x86_64-darwin17]
[2019-05-29 21:38:46] INFO WEBrick::HTTPServer#start: pid=86541 port=8080
Hello server!
::1 - - [29/May/2019:21:38:58 BST] "GET / HTTP/1.1" 200 14
- -> /
Hello server!
::1 - - [29/May/2019:21:38:59 BST] "GET /favicon.ico HTTP/1.1" 200 14
http://localhost:8080/ -> /favicon.ico
Hello server!
::1 - - [29/May/2019:21:39:03 BST] "GET / HTTP/1.1" 200 14
- -> /
Hello server!
::1 - - [29/May/2019:21:39:03 BST] "GET /favicon.ico HTTP/1.1" 200 14
http://localhost:8080/ -> /favicon.ico
```
Note that `puts("Hello server!")` still prints to the command line, and that
the `response.body` is what we see in the browser.
</details>
### Serving HTML
We usually want to respond with some HTML, not just "Hello browser!".
To make the browser treat our response as HTML we need to set an HTTP header
called `Content-Type` like this:
```ruby
server.mount_proc("/home") do |request, response|
response.content_type = "text/html; charset=utf-8"
response.body = "<h1>Hello browser!</h1>"
end
```
It can get difficult to maintain if we put all our HTML in ruby strings like
we did in the example above. It would be much nicer if we could have our HTML
in html files and our Ruby in ruby files. Fortunately, we can achieve this
with `File.read`:
```ruby
server.mount_proc("/home") do |request, response|
response.content_type = "text/html; charset=utf-8"
response.body = File.read("index.html")
end
```
(This assumes there's a file called `index.html` in your command line's working directory)
#### Task 6: Serve an HTML file with WEBRick
* Copy `lesson-1-html-and-css/index.html` into `lesson-3-ruby-on-the-web`
* Create a copy of `hello-world-server.rb` called `barking-permit-server.rb`
* Using the example above, make the server respond with the text from `index.html`
* Run the server with `ruby barking-permit-server.rb` and visit [http://localhost:8080/home](http://localhost:8080/home) to check it works
* When you're done, stop the server with `CTRL + C`
<details>
<summary>Answer</summary>
```ruby
require("webrick")
server = WEBrick::HTTPServer.new(Port: 8080, DocumentRoot: './public')
server.mount_proc("/home") do |request, response|
response.body = File.read("index.html")
end
server.start
```
</details>
### Responding to requests
Our "Apply for a Barking Permit" page contains a form.
Forms let the user provide the server with some input. When you submit the
form you should see the URL update to have `?dog-name=Fido&dog-age=3` at the
end.
What's happening is the browser is making a second HTTP request to the
server, but this time it's passing the user input in the URL (the bits after
the question mark are called query parameters).
We can access query parameters inside our WEBrick block using
`request.query["key"]` (note: square brackets, not round brackets)
```ruby
server.mount_proc("/home") do |request, response|
dog_name = request.query["dog-name"]
dog_age = request.query["dog-age"]
puts("The dog #{dog_name} is #{dog_age} years old")
response.body = File.read("index.html")
end
```
#### Task 7: different responses depending on the query
* Update `barking-permit-server.rb` to read user input with `request.query[]`
* Using `if request.query["dog-name"]` set the value of `response.body` to be
"The dog Fido is 3 years old" if the user provided a name, or
`File.read(...)` if not
* Run the server with `ruby barking-permit-server.rb`, visit [http://localhost:8080/home](http://localhost:8080/home) and submit the form
* When you're done, stop the server with `CTRL + C`
<details>
<summary>Answer</summary>
```ruby
require("webrick")
server = WEBrick::HTTPServer.new(Port: 8080, DocumentRoot: './public')
server.mount_proc("/home") do |request, response|
dog_name = request.query["dog-name"]
dog_age = request.query["dog-age"]
if dog_name
response.body = "The dog #{dog_name} is #{dog_age} years old"
else
response.body = File.read("index.html")
end
end
server.start
```
</details>
### Templating HTML
We've learned how to get the user's form input on the server, and change our
response depending on the input.
Our response to the user should be HTML as well though. If we're reading the
HTML response from a file how do we change the contents based on the user's
input?
Templating lets us take a file and substitute placeholders with values of our
choice. There are lots of fancy templating languages out there, but for us
Ruby's `.sub` method will be good enough.
Imagine we had some HTML in a file called `fruits.html`:
```html
<h1>Favourite Fruit</h1>
<p>My favourite fruit is FAVOURITE_FRUIT_NAME. I like it the best because it is FAVOURITE_FRUIT_REASON.</p>
```
We could load this file up, and then substitute the placeholders like this:
```ruby
File.read("fruits.html").
sub("FAVOURITE_FRUIT_NAME", "Banana").
sub("FAVOURITE_FRUIT_REASON", "easy to peel")
```
Which would return:
```html
<h1>Favourite Fruit</h1>
<p>My favourite fruit is Banana. I like it the best because it is easy to peel.</p>
```
#### Task 8: Template barking permit
Create a file called `barking-permit-template.html` in `lesson-3-ruby-on-the-web`.
Add the following HTML to the file (it's fine to copy-paste this):
<details>
<summary>HTML for `barking-permit-template.html`</summary>
```html
<!doctype html>
<html>
<head>
<title>Apply for a Barking Permit</title>
<link rel="stylesheet" href="https://www.govukpaas.app/govuk-frontend.css">
<style>
.govuk-barking-permit table {
font-family: Arial, sans-serif;
border-collapse: collapse;
width: 100%;
}
.govuk-barking-permit td,th {
border: 1px solid black;
text-align: center;
}
</style>
</head>
<body>
<div class="govuk-width-container">
<h1 class="govuk-heading-xl">Apply for a Barking Permit</h1>
<div class="govuk-grid-row">
<div class="govuk-grid-column-two-thirds">
<div class="govuk-panel govuk-panel--confirmation">
<h1 class="govuk-panel__title">
Application complete
</h1>
<div class="govuk-panel__body">
Your permit is available below
</div>
</div>
<div class="govuk-barking-permit">
<table>
<tr><th colspan="3">Barking Permit</th></tr>
<tr><td rowspan="6"><img src="images/puppy-in-a-hat.jpg" alt="A puppy in a hat"></td></tr>
<tr><th>Name</th><td>DOG_NAME</td></tr>
<tr><th>Age (human years)</th><td>DOG_AGE_HUMAN_YEARS</td></tr>
</table>
</div>
</div>
</div>
</div>
</body>
</html>
```
</details>
Update `barking-permit-server.rb` so that when `request.query["dog-name"]` is
set, `response.body` is set to `File.read("barking-permit-template.html")`.
Use `.sub` to replace `DOG_NAME` with `request.query["dog-name"]` and a
second `.sub` to replace `DOG_AGE_HUMAN_YEARS` with `request.query["dog-age"]`.
Run the server with `ruby barking-permit-server.rb`, visit [http://localhost:8080/home](http://localhost:8080/home) and submit the form.
When you're done, stop the server with `CTRL + C`.
<details>
<summary>Answer</summary>
```ruby
require("webrick")
server = WEBrick::HTTPServer.new(Port: 8080, DocumentRoot: './public')
server.mount_proc("/home") do |request, response|
dog_name = request.query["dog-name"]
dog_age = request.query["dog-age"]
if dog_name
response.body = File.read("barking-permit-template.html").
sub("DOG_NAME", dog_name).
sub("DOG_AGE_HUMAN_YEARS", dog_age)
else
response.body = File.read("index.html")
end
end
server.start
```
</details>
### Finishing off the Barking Permit
Following Task 8, our Barking Permit has:
* Name
* Age (human years)
We could use Ruby to add some more rows to the permit and make it a bit more
useful. For example: age in Dog Years (from lesson 2) and how long the permit
is valid for.
#### Task 9: Add rows to the Barking Permit
* Add rows (`<tr>...</tr>`) with template placeholders to `barking-permit-template.html` for:
* Age (dog years) (`DOG_AGE_DOG_YEARS`)
* Issued on (`PERMIT_ISSUED_ON`)
* Valid until (`PERMIT_VALID_UNTIL`)
* Calculate values for the placeholders using Ruby
* Age (dog years) should be the Age in human years multiplied by 7 (as per lesson 2)
* Issued on should be today's date, formatted as `Day/Month/Year`
* Valid until should be today's date + 1,000 days (arbitrarily), formatted as `Day/Month/Year`
* Use three more `.sub`s to replace the placeholders
* Run the server with `ruby barking-permit-server.rb`, visit [http://localhost:8080/home](http://localhost:8080/home) and submit the form.
* When you're done, stop the server with `CTRL + C`.
<details>
<summary>Answer</summary>
```ruby
require("webrick")
require("date")
def convert_to_dog_years(age_human_years)
return age_human_years * 7
end
server = WEBrick::HTTPServer.new(Port: 8080, DocumentRoot: './public')
server.mount_proc("/home") do |request, response|
dog_name = request.query["dog-name"]
dog_age = request.query["dog-age"]
if dog_name
response.body = File.read("barking-permit-template.html").
sub("DOG_NAME", dog_name).
sub("DOG_AGE_HUMAN_YEARS", dog_age).
sub("DOG_AGE_DOG_YEARS", convert_to_dog_years(dog_age.to_i).to_s).
sub("PERMIT_ISSUED_ON", Date.today.strftime("%d/%m/%Y")).
sub("PERMIT_VALID_UNTIL", (Date.today + 1000).strftime("%d/%m/%Y"))
else
response.body = File.read("index.html")
end
end
server.start
```
</details>
### Warning: security!
We've deliberately cut some corners by using `.sub` for our templating
which would cause security problems in a production application.
In particular, mixing user input and HTML can leave us vulnerable to a thing
called [Cross-site scripting](https://en.wikipedia.org/wiki/Cross-site_scripting).
You don't need to worry about this for the purposes of this lesson, but if
you're building a production website make sure to talk to a developer to
check you're following good security practices.
Frameworks like [Ruby on Rails](https://rubyonrails.org/) and
[Sinatra](http://sinatrarb.com/) help you avoid making security mistakes like
this.
## Congratulations!
If you've made it this far, give yourself a massive pat on the back! These
three lessons have been a whirlwind introduction to the world of programming
on the web. Don't worry if it things still seem confusing - programming is a
skill that takes time to learn.
Programming is amazingly useful now that computers are such a big part of our
lives, but it can also be great fun. Hopefully this is just the start of your
learning journey!
## Further reading
* [http://tutorials.codebar.io/](http://tutorials.codebar.io/)
* [http://railsgirls.com/](http://railsgirls.com/)
You can’t perform that action at this time.