Rapid Prototyping with Rails: Lesson 4, part 2
12 Nov 2014Items covered:
- Slugging
- Single Admin Role
- Timezones
- Users select their own timezone
Slugging
a custom URL generated based off of some characteristic of the page being viewed.
In our case, we care about from a slugging perspective:
- Posts
- Categories
- Users
What are the benefits of Slugging:
- SEO friendly/user ease
- Security (exposing primary key ids)
- Prevent those from knowing how many users you have
To set up a slug:
#posts/_post.html.erb
<span>
<%= link_to('#{post.comments.size} comments', post_path(post))
</span>
* the post_path(post) is actually calling the to_params method on the post object:
<span>
<%= link_to ('#{post.comments.size} comments', post_path(post.to_params)) %>
</span>
1. So how can we change the to_params method?
Within our model, we can declare an instance method where the to_params method will go to our slug instead of the params id.
def to_param
self.slug
end
2. We must also create a migration to add our slug column to our existing table:
rails g migration add_slug_to_posts
3. Open Migration File
change :posts |t| do
add_column :posts, :slug, :string
end
4. In Model, we must create a method to generate the slug.
Take time to create a method using gsub/regex (for edge cases)
test out regex on rubylur.com
One example:
def generate_slug
self.slug = self.title.gsub(' ', '-').downcase
self.save # we hate calling .save explicitly, maybe we can add it a callback.
end
5. ActiveRecord Callbacks (look up online, listed in order of workflow)
Methods the are exposed to use as a apart of the lifecycle of an ActiveRecord object, so we can insert or make changes to any of the callbacks.
A couple things to consider - do we want to generate the slug after created only, or after the post is created and updated?
We call insert add generate_slug method after @post.save:
after_save :generate_slug
But we dont want to create slugs off of bad urls!
So how about:
after_validations :generate_slug
or
before_save :generate_slug
- This code goes in the model
- Know the difference between
before_save - update slug whenever title changes
before_create
6. So if I go with before_save and then all my existing posts will blow up.
Why? because they do not have slugs. So I must go into to the Rails console and run Posts.all.each {|post| post.save} This is will trigger the before_save action and create slugs for all existing posts before saving.
The above console command is not good for when in production, better to run a migration.
7. Now when visiting the link:
<span>
link_to('#{post.comments.size} comments', post_path(post))
</span>
When using named routes, always use objects instead of hard-coding, bc on objects you can call to_params and its useful in case you want to sluggify.
Now we must update the setpost method in our postscontroller since we've changed the to_param method:
OLD:
def set_post
@post = Post.find(params[:id])
end
NEW:
def set_post
@post = Post.find_by(slug: params[:id])
end
8. Same thing goes for our comment which uses @post
#comments_controller
def create
@post = Post.find_by(slug: params[:post_id])
end
- One of the best slugging gems is friendly_id
9. Update the span id
<span id='post_<%=post.id%>_votes>' to be
<span id='post_<%=post.slug'%>_votes'> in your _post.html.erb partial and your
vote.js.erb
Single Admin Role
Define a number of roles and each roles has a set of permissions User has many roles, A Role has many permissions.
You must create has_many through association. Over all, having various roles with permissions is discouraged because every action is required to be checked against a set of permissions and this will not only greatly complicate development and slow the application down.
For simple apps, a single admin role is typically best This requires having a role column on users that takes a string as free form text, which allows you to specify whatever role you want. This doesnt allow for the most flexiiblity but will give you keep you from having to create a roles table and permissions table.
1. Create migration to add roles to users table:
rails g migration add_roles_to_users
2. Open migration file, add syntax
def change
add_column :users, :role, :string
end
3. Create roles in user model
def admin?
self.admin == 'admin'
end
def moderator?
self.role == 'moderator?'
end
- Or you can use a symbol for performance optimization
def admin?
self.role.to_sym == :admin
end
def moderator?
self.role.to_sym == :moderator
end
4. So what if we said "In order to create a category, you must be an admin"
We could create a beforeaction :requireadmin in the categories_controller
class CategoriesController < ApplicationController
before_action :require_user
before_action :require_admin
end
5. We want to create the requireadmin method in the applicationcontroller as
we did with requireuser. We want to make sure that the user is loggedin? . If the currentuser is an an admin but not loggedin, an exception will be thrown because you must consider nil condition.
def require_admin
access_denied unless logged_in? and current_user.admin?
end
def access_denied
flash[:error] = "Sawry.. you can't do that."
redirect_to root_path
end
6. Move New Category Link (and New Post Link) under if loggedin? under _navigation.htmlerb
#layout/_navigation.html.erb
<% if logged_in? and current_user.admin?
<li>
<%= link_to "New Post", new_post_path %>
</li>
<li>
<%= link_to "New Category", new_category_path %>
</li>
<% end %>
<% end %>
Timezones
1. We have an existing helpermethod in our applicationhelper.rb that displays timezone,
we can just add %Z to include timezone:
def display_datetime(dt)
dt.strftime("%m%d%Y %1:%M%P %Z")
end
2. In your application.rb file, uncomment out:
config-time_sone = 'Central Time (US & Canada)'
3. Now we need to find the string for setting the default time to Eastern Standard Time
- run rake -T | grep time => rake time:zones:all
- run rake time:zones:all => displays all timezones available for rails
- run rake time:zones:all | grep US => 'Eastern Time (US & Canada)'
4. Anytime you make changes to the application.rb file, you must restart the server
Users select their own timezone
This is great, but what if we want users to select their own timezone:
1. Create Rails migration add timezone column to users
rails g migration addtimezonesto_users
2. class AddTimeZonesToUsers < ActiveRecord::Migration
def change
add_column :users, :time_zones, :string
end
end
3. We want to add the ability for users to select their timezone upon registration:
<div class='control-group'>
<%= f.label :time_zone %>
<%= f.time_zone_select :time_zone, ActiveSupport::TimeZone.us_zones %>
</div>
This timezoneselect comes with Rails 4 only and newer. The ActiveSupport::TimeZone.us.zones will display all US timezones at the top. Reference documentation for more option
4. Check what data can be submitted by adding a binding.pry to the user#create
action and running params in rails console
5. userparams will show what params have been submitted, and we can see that timezone was not saved because we need to add it to strongparams
def user_params
params.require(:user).permit(:username, :password, :time_zone)
end
6. Once logged in, we want the users timezone to be displayed, we must edit the display_datetime method
in the ApplicationHelper:
module ApplicationHelper
def display_datetime(dt)
if logged_in? && !current_user.time_zone.blank?
dt = dt.in_time_zone(current_user.time_zone)
end
end
end
Time_zone method will display the object's time if its passed a string. So in rails console,if you run post.created_at.in_time_zone("Arizona") => will return the datetime object
To understand more about the timezoneselect or the intimezone method, look up the documentation.
7. If we wanted to specify the default time in our drop down
<div class='control-group'>
<%= f.label :time_zone %>
<%= f.time_zone_select :time_zone, ActiveSupport::TimeZone.us_zones, default: Time.zone.name%>
</div>