The final lesson (Lesson 4) of Course 2 involved adding some more functionality to the PostIt app (the Reddit clone).
1. 'AJAX'ifying the voting
In Rails, this is slightly different from the way this is implemented in a non-Rails based web application. Called Server-generated JavaScript Response (SJR), Rails can dynamically create a JavaScript that implements an AJAX action.
1 2 3 |
|
The remote: true
switch dictates AJAX implementation of that particular link.
Another thing to note is that the method: 'post'
switch dynamically (using a JavaScript) injects a form with 'POST' submission to the same link as indicated by the vote_post_path(post_object)
call.
The action that handles the new request needs to be modified to deal with a JavaScript response now:
1
2
3
4
5
6
7
8
9
10
11
12
respond_to do |format|
format.html do
if @vote.valid?
flash[:notice] = 'Your vote was cast.'
else
flash[:error] = 'You can vote only once on this post.'
end
redirect_to :back
end
format.js # by default renders the [action_name].js.erb template in the views/[controller_name] folder
end
The code in the [action_name].js.erb could be something as simple as:
$('#post_<%= @post.id %>_votebox').html("<%= @post.total_votes %>");
or a bit more complicated as in this file
2. 'Slug'ifying the routes
There is a security concern with URLs that look like 'ppj-postit.heroku.com/users/3' with the '3' representing the ID of the User object. Same is the case with Posts and Categories. A more appropriate URL for the above would be 'ppj-postit.heroku.com/users/testman' where 'testman' is automatically generated from the username
attribute of a user. This is what is called a 'slug' in web development jargon. To achieve this, the to_param()
method of the model object has to be over-ridden, because this is the method used by the named-route methods like user_path
, et al.
First a 'slug' column needs to be added to the models (tables) to store the respective slugs. The models also need to have a way to automatically generate and save the slug. The former can be done by simple column creating migration. The generation of the slug can also be done in the same migration (for existing records). The slug-generator will take the appropriate attribute of the model, the :username
attribute in case of the Users table.
Writing a generate_slug
method for the model will be useful for auto-generation of slugs. This method can then be called to create a slug every time a new User object is created using the before_save
ActiveRecord callback.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
1 2 3 4 |
|
Note: The implementation of generate_slug
shown above is very simplistic. A robust implementation should ensure that the slugs are never repeated for the same model. This is especially applicable for the models where slugs will be generated from a non-unique attribute (e.g. Posts.title). The code below shows exactly that:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
|
3. Simple Role Implementation
- add a 'role' columns to the Users table using a simple migration
- add methods to the model to check whether a particular user has a specific role (indicated by a string in the role column)
1 2 3
def admin? self.role == 'admin' end
- enable/disable features in the controllers…or in the view templates…
application_controller.rb 1 2 3
def require_admin access_denied "You need admin access to do that!" unless logged_in? and current_user.admin? end
based on the role checks1 2 3
<% if logged_in? and current_user.admin? %> <li><%= link_to "Create New Category", new_category_path %></li> <% end %>
A more serious implementation would call for defining a Role model and a Permission model. A has-many through associations will define Users can have many Roles through: :permissions
. The role checks can now be done through this association. Chris Lee, the instructor of the 2nd course suggests that this should be avoided unless absolutely necessary, because this rolls into a massive net of checks and nested models. The simple implementation shown above suffices our need.
4. Time-zone Setting
The app has a default time-zone (UTC) which can be overridden by specifying a different one in the './config/application.rb' file.
Adding config.time_zone = 'Melbourne'
to this file will set the default time-zone of the application to Melbourne time overriding the default (UTC)
Note: All available time-zones can be listed by running the rake task rake time:zones:all
.
Now that the application has a default time-zone, each user should be able to set his/her own. That would need a dedicated column to save that user's 'timezone' in. This can be added by a simple migration:
rails generate migration add_timezone_to_users
- Add time-zone column to the User table:
1 2 3 4 5
class AddTimezoneToUsers < ActiveRecord::Migration def change add_column :users, :timezone, :string end end
rake db:migrate
Time to modify the view template to allow setting of the time-zone: the common form partial used for User registration (User#new) and profile-update (User#edit). The following addition to the existing form partial will address this need:
1
2
3
4
<div class='control-group'>
<%= f.label :timezone %>
<%= f.time_zone_select :timezone, nil, default: Time.zone.name %>
</div>
The strong parameters need to include the new attribute :timezone
for the above to work through the web interface.
params.require(:user).permit(:username, :password, :timezone)
For time to be displayed in the logged in user's time-zone, the time-display helper now needs to be modified:
1 2 3 4 5 6 7 |
|
Refer to this commit to the PostIt app for details of code changes.