Resource Modeling
Created by Myriam Leggieri, @iammyr for Rails Girls Galway The basic guides that have been merged and adapted are the Ruby on Rails Tutorial, the basic RailsGirls app and the tutorials for creating thumbnails, authenticating users, adding design, deploying to OpenShift and adding comments.
What do we want our app to do? As a first thing, we would like to * authenticate users * allow authenticated users to create a new touristic place description * allow authenticated users to comment those places * allow authenticated users to rate up to which extent those places are autism-friendly or not.
Note that these requirements help us identify 4 different resources: user, place, comment, rating. We are now going to model them specifying their properties and their associations with each other.
We will enable the rating in the next tutorial.
Authenticated Tourists/Users
Let’s generate our first resource: user and require its authentication.
Step 0: Add devise gem
Open up your Gemfile
and add this line
gem 'devise'
and run
bundle install
to install the gem. Also remember to restart the Rails server.
Step 1: Set up devise in your app
Run the following command in the terminal.
rails g devise:install
Step 2: Configure Devise
Ensure you have defined default url options in your environments files. Open up config/environments/development.rb
and add this line:
config.action_mailer.default_url_options = { :host => 'localhost:3000' }
before the end
keyword.
Open up app/views/layouts/application.html.erb
and add:
<% if notice %>
<p class="alert alert-success"><%= notice %></p>
<% end %>
<% if alert %>
<p class="alert alert-danger"><%= alert %></p>
<% end %>
right above
<%= yield %>
Step 3: Setup the User model
We’ll use a bundled generator script to create the User model.
rails g devise user
rake db:migrate
Coach: Explain what user model has been generated. What are the fields? Note that a model inherits abilities to interact with the DB from its ActiveRecord::Base super-class (ref. MVC).
Step 4: Create your first user
Now that you have set everything up you can create your first user. Devise creates all the code and routes required to create accounts, log in, log out, etc.
Make sure your rails server is running, open http://localhost:3000/users/sign_up and create your user account.
Step 5: Add sign-up and login links
All we need to do now is to add appropriate links or notice about the user being logged in in the top right corner of the navigation bar.
In order to do that, edit app/views/layouts/application.html.erb
by adding at the beginning of the body:
<p class="navbar-text pull-right">
<% if user_signed_in? %>
Logged in as <strong><%= current_user.email %></strong>.
<%= link_to 'Edit profile', edit_user_registration_path, :class => 'navbar-link' %> |
<%= link_to "Logout", destroy_user_session_path, method: :delete, :class => 'navbar-link' %>
<% else %>
<%= link_to "Sign up", new_user_registration_path, :class => 'navbar-link' %> |
<%= link_to "Login", new_user_session_path, :class => 'navbar-link' %>
<% end %></p>
Finally, force the user to redirect to the login page if the user was not logged in. Open up app/controllers/application_controller.rb
and add:
before_action :authenticate_user!
after protect_from_forgery with: :exception
.
Open your browser and try logging in and out from.
Coach: Talk about the user_signed_in?
and current_user
helpers. Why are they useful?
Let’s add-commit-push to your GitHub repo! See how nicely all the changes are now on your GitHub profile? :)
Touristic Places
We now use Rails’ scaffold functionality to generate and set up all that is necessary to list, add, remove, edit, and view our second resource: “touristic places”.
rails generate scaffold place name:string address:string latitude:decimal longitude:decimal description:text picture:string user_id:integer
The scaffold creates new files in your project directory. However, we have defined (modeled) a “structure” for our “place” resource and we want all the future instances of this resource to stick to this structure and get stored somewhere, i.e., in a database.
We are already using a database (see gem 'sqlite'
in your Gemfile). Let’s add the structure of “place” as a table to our database with the following.
bin/rake db:migrate
ruby bin/rake db:migrate
Coach: What is Rails scaffolding? What are migrations and why do you need them? Note the pages that have been created to manipulate the “place” resources and their naming convention. Look at the server logging and explain it as a report of the following interaction (in the context of the MVC pattern): * The browser issues a request for the /places URL. * Rails routes /places to the index action in the Places controller. * The index action asks the Place model to retrieve all places (Place.all). * The Place model pulls all the places from the database. * The Place model returns the list of places to the controller. * The controller captures the users in the @users variable, which is passed to the index view. * The view uses embedded Ruby to render the page as HTML. * The controller passes the HTML back to the browser
Note that the controller created is RESTful (explain)
Note that the controller inherits abilities (large amount of functionality, such as the ability to manipulate model objects, filter inbound HTTP requests, and render views as HTML) from its ApplicationController super-class (ref. MVC).
Open up app/views/places/show.html.erb
and remove the line that says:
<p id="notice"><%= notice %></p>
This line is not necessary as we’ve already put the authenticated user notice in the app/views/layouts/application.html.erb
file.
Let’s add-commit-push to your GitHub repo!
Resource Associations
Note that places aren’t yet properly associated with users. For instance, when creating a new place the field “User” is expected to be filled by ourselves and when viewing a user profile there isn’t any list of places created by him/her and viceversa. Also, when deleting a user account all the places he/she created do not get deleted automatically.
Let’s properly create the 1-to-many association between User and Places.
Step 1. Add 1-to-many association
You need to make sure that Rails knows the relation between the User and Place resources. As one user can create many places we need to make sure the user model knows that. Open app/models/user.rb and after the row
class User < ActiveRecord::Base
add
has_many :places
The place also has to know that it belongs to a user. So open app/models/place.rb and after
class Place < ActiveRecord::Base
add the row
belongs_to :user
Step 2: Render the views
Open app/views/places/_form.html and after
<div class="field">
<%= f.label :user_id %><br>
<%= f.number_field :user_id %>
</div>
add the row
<%= f.hidden_field :user_id, :value => current_user.id %>
next, remove
<div class="field">
<%= f.label :user_id %><br>
<%= f.number_field :user_id %>
</div>
Step 3: Set edit/delete permissions
Allow only the place creator to edit/delete a place.
Open app/vies/places/index.html.erb and substitute
<td><%= link_to 'Edit', edit_place_path(place) %></td>
<td><%= link_to 'Destroy', place, method: :delete, data: { confirm: 'Are you sure?' } %></td>
with
<% if user_signed_in? %>
<% if current_user.id == place.user_id %>
<td><%= link_to 'Edit', edit_place_path(place) %></td>
<td><%= link_to 'Destroy', place, method: :delete, data: { confirm: 'Are you sure?' } %></td>
<% end %>
<% end %>
That’s it. Now view a user you have inserted to your application and there you should see the form for creating a place as well as deleting older places.
Place’s Comments
Just as well as we created a “place” resource and associated it with users, we can create a “comment” resource and associate it with places 9and with its author).
rails generate scaffold comment body:text user_id:integer place_id:integer
bin/rake db:migrate
Coach: show that the scaffold generator has updated the Rails routes file with a rule for the Review resource
Resource Association
Step 1. Add 1-to-many association
Open app/models/place.rb and after the row
belongs_to :user
add
has_many :comments
Open app/models/comment.rb and after
class Comment < ActiveRecord::Base
add the rows
belongs_to :user
belongs_to :place
Step 2: Render the views
Open app/views/comments/_form.html and substitute
<div class="field">
<%= f.label :user_id %><br>
<%= f.number_field :user_id %>
</div>
with the row
<%= f.hidden_field :user_id, :value => current_user.id %>
next, substitute
<div class="field">
<%= f.label :place_id %><br>
<%= f.number_field :place_id %>
</div>
with the row
<%= f.hidden_field :place_id%>
Open app/views/places/show.html.erb and just before the bottom links add
<h3>Comments</h3>
<% @comments.each do |comment| %>
<div>
<strong><%= comment.user_id %></strong>
<br />
<p><%= comment.body %></p>
<p><%= link_to 'Delete', comment_path(comment), method: :delete, data: { confirm: 'Are you sure?' } %></p>
</div>
<% end %>
<%= render 'comments/form' %>
In app/controllers/places_controller.rb add to show action after the row
@place = Place.find(params[:id])
this
@comments = @place.comments.all
@comment = @place.comments.build
Step 3: Set edit/delete permissions
Allow only the comment creator to edit/delete a comment.
Open app/views/places/show.html.erb and substitute
<p><%= link_to 'Delete', comment_path(comment), method: :delete, data: { confirm: 'Are you sure?' } %></p>
with
<% if user_signed_in? %>
<% if current_user.id == comment.user_id %>
<p><%= link_to 'Delete', comment_path(comment), method: :delete, data: { confirm: 'Are you sure?' } %></p>
<% end %>
<% end %>
Resource Field Validation
At the moment comments, places and users are characterized by information that is never validated for its correctness. Still, for instance, there should be a limit on the length of comments in review or on the format of a user’s email address.
Then let’s add a constraint over the length of the comment’s body field (we’ll use the ‘validates’ keyword). Open app/models/comment.rb and add between ‘class’ and ‘end’:
validates :body, length: { maximum: 140 }
Finetune the routes
If you try to open http://localhost:3000 it still shows the “Welcome aboard” page. Let’s make it redirect to the places page.
Open config/routes.rb
and after the first line add
root :to => redirect('/places')
Test the change by opening the root path (that is, http://localhost:3000/) in your browser.
Coach: Talk about routes, and include details on the order of routes and their relation to static files.
Rails 3 users: You will need to delete the index.html from the /public/
folder for this to work.