Ruby on Rails — the basics

Jeff P
26 min readNov 30, 2023

--

setting up with Windows

Today we’re going to try out Ruby on Rails in a Windows 10 OS.

We need to install Node — https://nodejs.org/en/

After this, we also need yarn:

npm install --global yarn

Then we need the Rails installer package 2.3:

Next we want to ensure we have the latest version of Ruby (with Devkit) for Windows installed:

click enter when prompted to accept all defaults.

we can check we’ve now got the latest version of Ruby by closing our command prompt, and then re-opening it and typing in:

ruby -v

Once confirmed we have the latest version of Ruby installed, then we finally upgrade rails from a command prompt, ensuring the command prompt is open as Administrator, or you’ll get errors:

gem update --system 3.4.22

gem install rails

once installed, check the version of rails installed:

rails -v

next we’ll setup a new application project called “friends” in our C:\Sites directory. Navigate there and type in the following:

rails new friends

Rails will now create a new directory “friends”. Inside this directory, Rails will generate a set of files and folders that make up the structure of a Rails application. This includes:

app/: Contains the controllers, models, views, helpers, mailers, and assets for your application.
config/: Contains configuration files for your application, databases, routes, etc.
db/: Contains your database schema and migrations.
Gemfile and Gemfile.lock: These files allow you to specify what gem dependencies are needed for your Rails application.

When the installation finishes, you can navigate to this new folder:

C:\Sites\friends\

We’re now ready to start up the rails server by typing in:

rails s

If all goes well, you should be able to navigate to 127.0.0.1:3000 in your browser and you’ll see the following:

Creating our first web page

Open up a second command prompt (leaving the first prompt running Rails server) and type in the following:

rails generate controller home index

When you run this command, several things happen:

C:\Sites\friends>rails g controller home index
create app/controllers/home_controller.rb
route get 'home/index'
invoke erb
create app/views/home
create app/views/home/index.html.erb
invoke test_unit
create test/controllers/home_controller_test.rb
invoke helper
create app/helpers/home_helper.rb
invoke test_unit

Controller Generation: Rails will generate a new controller named home. Controllers in Rails are responsible for handling the incoming web requests to your application, and then either rendering a view or redirecting to another action. In this case, the controller will be named Home.

Action Creation: Inside the home controller, Rails will also create an action called index. The “index” action is typically used to display a list of items or a dashboard. There are different types of actions in Rails, including “show”, which displays one specific item (e.g., a single blog post), “new”, which returns a form for creating a new item, “create”, which processes the form data submitted from the new action and creates a new item, as well as “edit”, “update” and “destroy”.

View Creation: Alongside the controller and action, Rails will create a view file for the index action. This file will be located in app/views/home/. The file will be named index.html.erb — This is where you will write the HTML (and Ruby, if needed) to be displayed when the index action of the home controller is invoked.

Assets: Rails will generate a JavaScript file (app/javascript/packs/home.js) and a CSS file (app/assets/stylesheets/home.css) for the controller. These files are where you can write JavaScript and CSS specifically for the views handled by the home controller.

You can now navigate to http://localhost:3000/home/index) in your web browser to see the results of the index action in the home controller, displayed using the index.html.erb view.

We can see the HTML of this page in our “app -> views -> home” folder, where the index.html.erb is stored.

Setting up a route

currently our new web page can be found at http://localhost:3000/home/index

but what if we want our page to be THE home page when we go to http://localhost:3000 ?

Well to do this, we need to create a route for it. So we go to config, and find the routes.rb file:

We’ll add in a special “root” route underneath our current route to index…

root 'home#index'

The # separates the controller name (home) and the action name (index). This syntax tells Rails which controller and action to use for a given route. In this case, it directs the root route of the application to the index action in the Home controller.

Now we should be able to see our page at http://localhost:3000

Let’s change the index page…. We’ll go back into app/views/home/index.html.erb

We’ll change it to the following:

<h1>Hello World</h1>
<p>Welcome to my new app!!</p>

Now we’ll save and reload the home page to see the changes.

If we actually inspect this page in the browser inspector, we won’t just see what HTML we typed…. there will be more stuff in there…

All this extra stuff actually comes from the page template, which is in app/views/layouts/application.html.erb

application.html.erb

There’s a very special tag in this file:

<%= yield %>

The <%= yield %> statement is where the content of other view templates (associated with different controller actions) is rendered. It serves as a placeholder where the content of other view templates is inserted into the layout. This is where our HTML from the index.html.erb is being injected.

We can actually prove this, by making some changes to the application.html file, which would then be reflected on all files we create.

So for example, let’s change the application.html.erb file to the following:

<!DOCTYPE html>
<html>
<head>
<title>Friends</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<%= csrf_meta_tags %>
<%= csp_meta_tag %>

<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
<%= javascript_importmap_tags %>
</head>

<body>
<p>This would be above the content of all pages....</p>
<%= yield %>
<p>This would be below the content of all pages....</p>
</body>
</html>

If we look on our page, we’d of course see the change….

But let’s also create a completely new page manually, just to prove this. In order to create a new page manually, we need a minimum of three things.

  1. a new .html.erb file in our app/views/home/ folder
  2. defining the new page in our controllers/home_controller.rb file

3. a route to get to our page in the config/routes.rb file

We’ll create a new page called “about”

Now we’ll define it in the controllers/home_controller.rb file

and finally add a route to it in the the config/routes.rb file

now if we load up http://127.0.0.1:3000/home/about we’ll see the following, with the same text above and below what we found on the index page, directly via application.html.erb

Partials

We’re now going to talk about “partials”, but before we do this, let’s quickly use Bootstrap to make our page a little bit prettier.

Inside the application.html.erb file, we’ll add the necessary CSS and JS for Bootstrap, found here:

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Bootstrap demo</title>

<!-- maintain special ERB embedded code from application.html.erb -->
<%= csrf_meta_tags %>
<%= csp_meta_tag %>

<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
<%= javascript_importmap_tags %>
<!-- end -->


<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
</head>
<body>
<h1>Hello, world!</h1>

<!-- our yield tag for HTML injection -->
<%= yield %>
<!-- end -->


<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL" crossorigin="anonymous"></script>
</body>
</html>

Note that we kept the lines of code that use the special embedded ruby injection tags <%= … %>

You’ll know it worked as the font styling will have changed….

Now we’ve got Bootstrap running, let’s focus on “partials”. Partials are a feature of the view layer that allow you to break down complex views into more manageable, reusable pieces. A partial is essentially a smaller, reusable view template that can be included in other view templates.

To create a “partial”, we create a file, but use an underscore _ in front of the name. For example, _header.html.erb

Let’s create two partials….one for a header, and one for a footer…

Now we can inject them using the erb “render” tag in our application.html.erb file as follows:

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Bootstrap demo</title>

<!-- maintain special ERB embedded code from application.html.erb -->
<%= csrf_meta_tags %>
<%= csp_meta_tag %>

<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
<%= javascript_importmap_tags %>
<!-- end -->


<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
</head>
<body>


<%= render "home/header" %>

<!-- our yield tag for HTML injection -->
<%= yield %>
<!-- end -->

<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL" crossorigin="anonymous"></script>
</body>
</html>

Note that when we use the render tag, we don’t mention the underscore…..Rails already knows how to treat the file as a “partial” based on the fact that we named it _header

This is now what we see:

Creating a Nav Bar

Let’s now replace the content of the _header file we just created with a NavBar from Bootstrap…

We’ll copy the Bootstrap Navbar code straight into our file…

Now when we reload our page, we’ll see this…

I’ll change where it says Navbar to “Friends App” and also change the navbar theme to dark….

<nav class="navbar navbar-expand-lg bg-body-tertiary" data-bs-theme="dark">
<div class="container-fluid">
<a class="navbar-brand" href="#">Friend App</a>

This gives us this….

We’ll also get rid of some of the navbar elements that we won’t use…. we’ll get rid of the dropdown and also the “Disabled” link….

<nav class="navbar navbar-expand-lg bg-body-tertiary" data-bs-theme="dark">
<div class="container-fluid">
<a class="navbar-brand" href="#">Friend App</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link active" aria-current="page" href="#">Home</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Link</a>
</li>
</ul>
<form class="d-flex" role="search">
<input class="form-control me-2" type="search" placeholder="Search" aria-label="Search">
<button class="btn btn-outline-success" type="submit">Search</button>
</form>
</div>
</div>
</nav>

link_to

With normal HTML we’d use something like <a href=”page”>

However for Rails, we use the link_to tag….

To demonstrate how they work, we’ll change the “Home” link on the navbar to point to our home page, and the “Link” on the navbar to “About” to point to our “about” page.

Inside the _header file we created, we’ll first remove the code for “Home” and replace with a link_to

replace this…..

        <li class="nav-item">
<a class="nav-link active" aria-current="page" href="#">Home</a>
</li>

with this….

        <li class="nav-item">
<%= link_to "Home", root_path, class: "nav-link" %>
</li>

Now we’ll do the same thing for the “Link” part of the navbar.

replace this….

        <li class="nav-item">
<a class="nav-link" href="#">Link</a>
</li>

with this….

        <li class="nav-item">
<%= link_to "About", home_about_path, class: "nav-link" %>
</li>

now you’ll be able to toggle between our two pages using the nav bar links…

So in terms of using the link_to tag, for the “about” page, we used home_about_path which is the home controller, the about page, and then _path

The home page simply just uses root_path

Scaffolding CRUD

In the context of Ruby on Rails, CRUD represents the following:

Create: This operation refers to the ability to create new records or resources in the database. In Rails, this is typically accomplished through the use of a form that collects user input and then creates a new database record when the form is submitted.

Read: Reading, or retrieving, data from the database is the most common operation in a web application. It involves fetching and displaying existing records or resources. In Rails, this is often done by retrieving data from the database and rendering it in views.

Update: Updating records or resources allows you to modify existing data. In Rails, you typically use forms to edit and update data. The updated information is then saved back to the database.

Delete: Deleting records or resources involves removing them from the database. In Rails, you can implement this functionality through a delete action in your controller, which removes the specified record from the database.

Whenever we create a database, we can get Rails to generate it using a barebones “scaffold”. We do this with the following command:

rails g scaffold

We then add to this a database name, and the keys for the database we want to use as well as their data-types (e.g. string, integer, float, boolean, etc)

So let’s use the following scaffold for our database, with a table name of “friend”

rails g scaffold friends first_name:string last_name:string email:string 
phone:string twitter:string

If we now head to our db/migrate folder, we’ll see a migration file with all of the details we created:

When you develop a web application, you often need to make changes to your database schema, such as adding new tables, modifying existing columns, or creating indexes. Migrations provide a way to define these changes in a code file, so you can easily apply them to your database.

Version Control: Migrations are versioned. If you look at the name of the file, you’ll see that each migration file has a timestamp in its filename, and Rails keeps track of which migrations have been applied to the database. This ensures that you can update your development, test, and production databases consistently, even as your application evolves.

Database Consistency: Using migrations helps maintain database consistency across different environments and among team members. Everyone can run the same set of migrations to ensure that their databases have the same schema structure.

Rollbacks: Migrations also allow you to roll back changes. If you discover a problem with a migration, you can create another migration to undo the changes, making it easy to revert to a previous state of the database.

We now need to push this migration into the actual database so that we have a schema.

rails db:migrate

We should now have a schema…

Also note how it automatically pluralized our table name to “friends” even though we specified it to be “friend”.

If we look in our app/views folder, we’ll see a folder called “friends” with a bunch of new files in it….

You’ll see in this folder, there is an index.html.erb file. Let’s navigate to this in our browser, at http://127.0.0.1:3000/friends/

We can now click on “New Friend”….. to open the “new” page.

If we have a look at the code for this “new” page, we’ll see the following:

So it’s rendering content from the _friend page, and it has added a link_to to go back to the main friends index page.

Let’s take a closer look at the _friend page

We can now see all the HTML that was rendered to the page….

Let’s try filling in the form on the web page….

When we fill out the form and click “Create friend” we’ll get confirmation it was added.

This is the CREATE part of CRUD.

IF we click on “Edit this friend” we’ll see the UPDATE part of CRUD.

if we click “show this friend”, we’ll see the READ part of CRUD

and of course if we were to click on “Destroy this friend”, that would be the DELETE part of CRUD.

Let’s add a few more friends and then go back to the main page….

Let’s also add some links to our navbar

<nav class="navbar navbar-expand-lg bg-body-tertiary" data-bs-theme="dark">
<div class="container-fluid">
<a class="navbar-brand" href="#">Friend App</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<%= link_to "Home", root_path, class: "nav-link" %>
</li>
<li class="nav-item">
<%= link_to "About", home_about_path, class: "nav-link" %>
</li>
<li class="nav-item">
<%= link_to "Friends", friends_path, class: "nav-link" %>
</li>
<li class="nav-item">
<%= link_to "New Friend", new_friend_path, class: "nav-link" %>
</li>
</ul>
<form class="d-flex" role="search">
<input class="form-control me-2" type="search" placeholder="Search" aria-label="Search">
<button class="btn btn-outline-success" type="submit">Search</button>
</form>
</div>
</div>
</nav>

which should give us this….

We can of course apply more styling to this page as well and make it look better.

Let’s first amend the index page…

<div id="friends">
<table class="table">
<thead>
<tr>
<th scope="col">First name:</th>
<th scope="col">Last name:</th>
<th scope="col">Email:</th>
<th scope="col">Phone:</th>
<th scope="col">Twitter:</th>
<th scope="col"></th>
</tr>
</thead>
<tbody>
<% @friends.each do |friend| %>
<tr>
<td><%= friend.first_name %></td>
<td><%= friend.last_name %></td>
<td><%= friend.email %></td>
<td><%= friend.phone %></td>
<td><%= friend.twitter %></td>
<td><%= link_to "Show", friend %></td>
</tr>
<% end %>
</tbody>
</table>
</div>

<%= link_to "New friend", new_friend_path %>

which give us this….

Using “Gems”

We can use gems as handy little helper modules to do things for us, in a similar way to when we install things via npm.

We’ll install a gem called “devise” to help us create a login framework for our project.

let’s add this to our gemfile by copying to our clipboard…

and pasting into our gemfile

now we’ll install it from the terminal with bundle install

bundle install

For this particular gem, we also need to run another command

rails generate devise:install

You’ll then see the following output in the terminal…

C:\Sites\friends>rails generate devise:install
create config/initializers/devise.rb
create config/locales/devise.en.yml
===============================================================================

Depending on your application's configuration some manual setup may be required:

1. Ensure you have defined default url options in your environments files. Here
is an example of default_url_options appropriate for a development environment
in config/environments/development.rb:

config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }

In production, :host should be set to the actual host of your application.

* Required for all applications. *

2. Ensure you have defined root_url to *something* in your config/routes.rb.
For example:

root to: "home#index"

* Not required for API-only Applications *

3. Ensure you have flash messages in app/views/layouts/application.html.erb.
For example:

<p class="notice"><%= notice %></p>
<p class="alert"><%= alert %></p>

* Not required for API-only Applications *

4. You can copy Devise views (for customization) to your app by running:

rails g devise:views

* Not required *

We need to follow these instructions, so the first step is to copy the following line of code into the config/environments/development.rb file

config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }

step 2 we can skip, as we already have a route to index, which we created near the start.

step 3 is to add flash messages to our application file….

    <!-- flash messaeges as per devise installation rpocess -->
<p class="notice"><%= notice %></p>
<p class="alert"><%= alert %></p>

step 4 is to run the following command:

rails g devise:views

Now we need to setup a table for registration for users with the following command:

rails generate devise user

and now finally we need to push this migration as we did before…

rails db:migrate

We’re now installed and ready to go.

If we type in rails routes in the command line, we can see the routes created for us…

rails routes  

So for example, we can see that a route for /users/sign_up was created.

Let’s take a look at this in the browser with this URL

http://127.0.0.1:3000/users/sign_up

Let’s also have a look at sign_in

Here is the key route info for the new routes…

new_user_session GET          /users/sign_in(.:format)      devise/sessions#new
new_user_registration GET /users/sign_up(.:format) devise/registrations#new
edit_user_registration GET /users/edit(.:format) devise/registrations#edit
destroy_user_session DELETE /users/sign_out(.:format) devise/sessions#destroy

lets add links for these routes to the navbar, remembering to end the route with _path as we did before.

<nav class="navbar navbar-expand-lg bg-body-tertiary" data-bs-theme="dark">
<div class="container-fluid">
<a class="navbar-brand" href="#">Friend App</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<%= link_to "Home", root_path, class: "nav-link" %>
</li>
<li class="nav-item">
<%= link_to "About", home_about_path, class: "nav-link" %>
</li>
<li class="nav-item">
<%= link_to "Friends", friends_path, class: "nav-link" %>
</li>
<li class="nav-item">
<%= link_to "New Friend", new_friend_path, class: "nav-link" %>
</li>
<li class="nav-item">
<%= link_to "Sign In", new_user_session_path, class: "nav-link" %>
</li>
<li class="nav-item">
<%= link_to "Sign Up", new_user_registration_path, class: "nav-link" %>
</li>
<li class="nav-item">
<%= link_to "Edit Profile", edit_user_registration_path, class: "nav-link" %>
</li>
<li class="nav-item">
<%= link_to "Sign Out", destroy_user_session_path,
data: { turbo_method: :delete, turbo_confirm: 'Are you sure?' }, class: "nav-link" %>
</li>
</ul>
<form class="d-flex" role="search">
<input class="form-control me-2" type="search" placeholder="Search" aria-label="Search">
<button class="btn btn-outline-success" type="submit">Search</button>
</form>
</div>
</div>
</nav>

Note that the sign out code is slightly different. We’re actually destroying the login session, so we use DELETE instead of GET, and in order to utilize this, we have to add data: { turbo_method: :delete, turbo_confirm: ‘Are you sure?’ } to the code.

With this code, you’re specifying the turbo_method: :delete attribute, which tells Turbo to send a DELETE request when the link is clicked, and turbo_confirm provides a confirmation prompt when the user clicks the link.

Hotwire Turbo (formerly known as Turbo) is designed to replace many aspects of Rails’ Unobtrusive JavaScript (UJS) with a more modern and efficient approach to handling client-side interactions in web applications.

Now we should be able to utilize the new links…

Rails Associations

We now want to associate our friends model (db) with our user model (db) — We want to associate them with each other through a “one-to-many” relationship.

In our Friend Model (friend.rb) we set the belongs_to association with the User model, indicating that each friend record is associated with a single user.

class Friend < ApplicationRecord
belongs_to :user
end

In our User Model (user.rb) we set the has_many association with the Friend model, indicating that a user can have multiple friends. This creates a one-to-many relationship, where each user can have many friends, but each friend belongs to one user.

class User < ApplicationRecord
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable

# create a one-to-many association
has_many :friends
end

The next thing we need to do is create a migration in the terminal:

rails g migration add_user_id_to_friends user_id:integer:index

this creates the following:

invoke  active_record
create db/migrate/20231128162626_add_user_id_to_friends.rb

If we now visit this file, we can see the following:

class AddUserIdToFriends < ActiveRecord::Migration[7.1]
def change
add_column :friends, :user_id, :integer
add_index :friends, :user_id
end
end

So we’ve added a column called user_id, and we’ve also created an index that will help speed up searching.

now we need to push the migration..

rails db:migrate

after pushing the migration, we’ll see the updates in our schema:

ActiveRecord::Schema[7.1].define(version: 2023_11_28_162626) do
create_table "friends", force: :cascade do |t|
t.string "first_name"
t.string "last_name"
t.string "email"
t.string "phone"
t.string "twitter"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.integer "user_id"
t.index ["user_id"], name: "index_friends_on_user_id"
end

create_table "users", force: :cascade do |t|
t.string "email", default: "", null: false
t.string "encrypted_password", default: "", null: false
t.string "reset_password_token"
t.datetime "reset_password_sent_at"
t.datetime "remember_created_at"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["email"], name: "index_users_on_email", unique: true
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
end

end

Next we need to add a field into our new friend form, so that we know which user id should be associated with the new friend…

<%= form_with(model: friend) do |form| %>
<% if friend.errors.any? %>
<div style="color: red">
<h2><%= pluralize(friend.errors.count, "error") %> prohibited this friend from being saved:</h2>

<ul>
<% friend.errors.each do |error| %>
<li><%= error.full_message %></li>
<% end %>
</ul>
</div>
<% end %>

<div>
<%= form.label :first_name, style: "display: block" %>
<%= form.text_field :first_name %>
</div>

<div>
<%= form.label :last_name, style: "display: block" %>
<%= form.text_field :last_name %>
</div>

<div>
<%= form.label :email, style: "display: block" %>
<%= form.text_field :email %>
</div>

<div>
<%= form.label :phone, style: "display: block" %>
<%= form.text_field :phone %>
</div>

<div>
<%= form.label :twitter, style: "display: block" %>
<%= form.text_field :twitter %>
</div>

<div>
<%= form.label :user_id, style: "display: block" %>
<%= form.text_field :user_id, value: current_user.id %>
</div>

<div>
<%= form.submit %>
</div>
<% end %>

So our form text field is :user_id and we use the devise helper of current_user.id to tag which user id is creating the new friend.

We should then see the following when we create a new friend…

So essentially the new friend will (in this instance) be associated with user_id 2

There are still some other changes we need to make. We need to change the form field type from text to number. We need to hide both the label and the text field form the screen. We also need to assign the id value to the friend_user_id db.

  <div>
<%= form.label :user_id, style: "display: none" %>
<%= form.number_field :user_id, id: :friend_user_id, value: current_user.id, type: :hidden %>
</div>

id: :friend_user_id sets the HTML id attribute of the input field to "friend_user_id."

Essentially, this is creating the following HTML:

<input name="friend[user_id]" id="friend_user_id" value="current_user_id" type="hidden">

If you try to add a friend at this point you’ll see an error….

This is because we need to extend the list of trusted parameters in our friends_controller.rb file:

    # Only allow a list of trusted parameters through.
def friend_params
params.require(:friend).permit(:first_name, :last_name, :email, :phone, :twitter)
end

As you can see, it doesn’t include the user_id parameter we added. So let’s add this on as well.

    # Only allow a list of trusted parameters through.
def friend_params
params.require(:friend).permit(:first_name, :last_name,
:email, :phone, :twitter, :user_id)
end

Now it should work:

Now we created this new friend with the “bob” account. We logged in as bob and created a new friend called jim, and now jim is one of the friends in bobs account.

But what happens if we create a new account and then check what friends we have? We’ll create a new account called Sally…

Now look what happens when we go to friends…

It’s showing jim as being sally’s friend, which is not right….jim is bob’s friend.

Even worse still, sally can edit the details of jim!

Let’s fix this…

In the friends_controller.rb file, we’ll add a before_action to prevent this from happening.

before_action is a method in Ruby on Rails that allows you to define filters that run before certain controller actions. These filters are used to perform actions such as authentication, authorization, setting up variables, or any other operations that need to occur before a specific controller action is executed.

lass FriendsController < ApplicationController
before_action :set_friend, only: %i[ show edit update destroy ]
before_action :authenticate_user!, except: [:index, :show]

So we now have a before_action that will only allow access to the index and show options if the user is not authenticated.

What we’re saying is that if a user is not (!) authenticated, don’t let them do anything except access the index and show pages.

So now if a user wasn’t logged in and opened a particular friend and then tried to edit that friend, they’d see a warning….

This resolves the issue with non-authenticated users, but it doesn’t stop a logged in user being able to delete or edit friends that wer created by other users. We’ll fix this next, but before we do that, let’s add another column into our friends table so we can see which user actually created the friend.

We’ll go into app/views/friends/index.html.rb and add another column…

<div id="friends">
<table class="table">
<thead>
<tr>
<th scope="col">First name:</th>
<th scope="col">Last name:</th>
<th scope="col">Email:</th>
<th scope="col">Phone:</th>
<th scope="col">Twitter:</th>
<th scope="col">User Id:</th>
<th scope="col"></th>
</tr>
</thead>
<tbody>
<% @friends.each do |friend| %>
<tr>
<td><%= friend.first_name %></td>
<td><%= friend.last_name %></td>
<td><%= friend.email %></td>
<td><%= friend.phone %></td>
<td><%= friend.twitter %></td>
<td><%= friend.user_id %></td>
<td><%= link_to "Show", friend %></td>
</tr>
<% end %>
</tbody>
</table>
</div>

<%= link_to "New friend", new_friend_path %>

Ok so now we can see the associated user id with the friend, but it still doesn’t stop one user amending or deleting the friend of another user. In order to fix this we need to create a new method in our friends_controller.rb file

  def correct_user
@friend = current_user.friends.find_by(id: params[:id])
redirect_to friends_path, notice: "Not authorized to edit this friend" if @friend.nil?
end

As per the devise documentation, For the current signed-in user, this helper is available:

current_user

So we’re defining an instance variable called @friend, and for the instance, we’re checking that the id value of the friend is associated with current_user based on the users id (found in the URL)

Perhaps an easier way to understand this is to add more friends…..

So the first friend was added by bob (User ID 2) and the rest were added by Chris (User ID 4)

It’s useful to be able to query the SQLite database using something like DB Browser for SQLite…

Once installed, you can open the SQLITE database, found in C:\Sites\friends\storage

In DB broswer you can toggle between the friends table and the users table….

So we can see that friend jim has a unique id of 5 in the table, and was created by user ID 2 (bob@bob.com)

So now going back to the method we created….

def correct_user
@friend = current_user.friends.find_by(id: params[:id])
redirect_to friends_path, notice: "Not authorized to edit this friend" if @friend.nil?
end

if the friend displayed is friend with id 5 (in the URL) …..

then the friend, needs to have been created by the current_user (which could currently be 1,2,3, or 4)

So basically, if the friend is 5, and the current user logged in, has a user_id of 2, then the friends instance will be 5…. however if the current_user is not 2, then it won’t be able to find the friend, in which case “nil” is assigned to @friend in Ruby (nil is like null in JavaScript) so if @friend is nil, then the notice of “Not authorized to edit this friend” will appear.

Note that in Ruby on Rails, params is a built-in method that provides access to data sent by the client (typically through an HTTP request) to your application. It allows you to access information such as form inputs, URL parameters, and other request-related data. params is often used to extract and process data submitted by users in web applications.

When you send data to your Rails application through URLs (e.g., in the query string), params allows you to access these parameters. For example, if you have a URL like /users/1, you can use params[:id] to access the value 1.

Form Data: When a user submits a form on a web page, the data entered into form fields is included in the params hash. You can retrieve form data using the names of the form fields as keys in the params hash.

Controller Actions: In Rails, when a request is routed to a specific controller action (e.g., create, update, show), params contains information about the request, such as the HTTP method (GET, POST, etc.) and any data submitted with the request.

Route Parameters: If you define dynamic route segments in your config/routes.rb file, params captures values from those dynamic segments. For example, if you define a route like /users/:id, then params[:id] will contain the value provided in the URL.

Wrapping up

We also need to add another before_action for this, at the top of the page…

class FriendsController < ApplicationController
before_action :set_friend, only: %i[ show edit update destroy ]
before_action :authenticate_user!, except: [:index, :show]
before_action :correct_user, only: [:edit, :update, :destroy]

So we’re saying that only for the correct_user, give access to edit, update and destroy, which are methods already created automatically further down….

  def edit
end

def update
respond_to do |format|
if @friend.update(friend_params)
format.html { redirect_to friend_url(@friend), notice: "Friend was successfully updated." }
format.json { render :show, status: :ok, location: @friend }
else
format.html { render :edit, status: :unprocessable_entity }
format.json { render json: @friend.errors, status: :unprocessable_entity }
end
end
end

def destroy
@friend.destroy!

respond_to do |format|
format.html { redirect_to friends_url, notice: "Friend was successfully destroyed." }
format.json { head :no_content }
end
end

We also need to amend the “new” and “create” methods…

  # GET /friends/new
def new
#@friend = Friend.new
@friend = current_user.friends.build
end

# GET /friends/1/edit
def edit
end

# POST /friends or /friends.json
def create
#@friend = Friend.new(friend_params)
@friend = current_user.friends.build(friend_params)

Changing from Friend.new to current_user.friends.build is a Rails best practice when you want to create records that are associated with other records, as it ensures data consistency and alignment with your data model.

By using current_user.friends.build, you automatically associate the new friend with the current user, indicating that the friend belongs to that user. This ensures that when you save the friend record, it will have the correct user ID in its database column that represents the owner.

Now if we try to edit a friend that is not associated with the correct user, we’ll see the error message. So for example, if we’re currently logged in as Chris, but try to edit the friend jim (which was created by user bob) we’ll see this…

Will create another medium posting for uploading to Github and hosting on Heroku

--

--

Jeff P
Jeff P

Written by Jeff P

I tend to write about anything I find interesting. There’s not much more to it than that really :-)

No responses yet