Rail3 | How to create standard route/action for ALL controllers? - ruby-on-rails-3

Well, DRY! So i thought it should be easy to add a new action (like the existing new, edit) to all my controllers (in my case copy). But how do you setup a new route for ALL controllers?
Without going in to 'loops' (i.e. %w().each ...) inside the routes.rb ?
I mean, we want DRY right? So you don't want copy your action inside the routes file for each resource. I guess you should be able to extend the default actions/routes (index, new, edit,etc.) easy?
Thanks!

AFIK no way to do this by default. You could monkey-patch resources to include this functionality:
https://github.com/rails/rails/blob/b229bc70e50ec0887c5bb3aaaa9c6ee8af054026/actionpack/lib/action_dispatch/routing/mapper.rb#L982
...but my hunch is you would be better off re-considering whether this functionality can be created another way, since what you want to do is "off the Rails".
One option is create a CloneController#new that accepts a model and id and creates a clone. This seems like it would be drier, and wouldn't require you to pepper a gazillion "clone_article" "clone_blog" "clone_user" paths all over the place.
Obviously you would want to carefully white-list the models/ids that can be passed in.

Looking through the source there isn't a way to add to the default actions for a resource.
But, as #juwiley says, the methods resources :item is just a shortcut for creating a load of member and collection methods.
All you need to do is something like this
class ActionDispatch::Routing::Mapper
def resources_with_copy(*resources, &block)
block_with_copy = lambda do
block.call
member do
post :copy
end
end
resources(*resources, &block_with_copy)
end
end
Then in your routes.rb just say
resources_with_copy :items
resources_with_copy :posts do
member do
post :share
end
end
...

Related

Rails + Devise - Session Controller explaination

I am playing around with Devise in a project, and am just trying to better understand how it all works. The Sessions controller in particular is doing a few things that I don't understand:
class Devise::SessionsController < ApplicationController
def new
# What benefit is this providing over just "resource_class.new"?
self.resource = resource_class.new(sign_in_params)
clean_up_passwords(resource)
# What is "serialize_options" doing in the responder?
respond_with(resource, serialize_options(resource))
end
def create
self.resource = warden.authenticate!(auth_options)
set_flash_message(:notice, :signed_in) if is_navigational_format?
sign_in(resource_name, resource)
respond_with resource, :location => after_sign_in_path_for(resource)
end
...
protected
...
def serialize_options(resource)
methods = resource_class.authentication_keys.dup
methods = methods.keys if methods.is_a?(Hash)
methods << :password if resource.respond_to?(:password)
{ :methods => methods, :only => [:password] }
end
def sign_in_params
devise_parameter_sanitizer.sanitize(:sign_in)
end
end
I assume that these methods are adding some sort of security. I'd just like to know what exactly they are protecting against.
The implementation of devise_parameter_sanitizer is creating a ParameterSanitizer instance. I often find looking at tests to be helpful in understanding the purpose of code; and this test I think illustrates it best-- since you're creating a new user, you don't want to allow users to assign any value they want to any parameter they want, so sanitize means "strip out any attributes other than the ones we need for this action". If this wasn't here, and you had a role attribute, a user could send a specially-crafted POST request to make themselves an admin when signing up for your site. So this protects against what's called, in the general case, a mass assignment vulnerability. Rails 3 and Rails 4 protect against this in different ways but the protections can still be turned off, and I'm guessing Devise is trying to set some good-practice defaults.
The serialize_options method is creating a hash of options to support rendering to XML or JSON. I found this out by looking at the implementation of responds_with, which calls extract_options! which uses the last argument as options if the last argument is a Hash. The documentation for responds_with says "All options given to #respond_with are sent to the underlying responder", so I looked at ActionController::Responder, whose documentation explains the process it takes to look for a template that matches the format, then if that isn't found, calling to_#{format}, then calling to_format. There's a view for HTML, and ActiveRecord objects respond to to_xml and to_json. Those methods use those options:
The <tt>:only</tt> and <tt>:except</tt> options can be used to limit the
attributes included, and work similar to the +attributes+ method.
To include the result of some method calls on the model use <tt>:methods</tt>.
So this keeps you from exposing more information than you might want to if someone uses the XML or JSON format.

How to user defined friendly URLs in Rails 3?

Now i have something like this
http://myapp.com/pages/1
http://myapp.com/pages/2
http://myapp.com/pages/3
http://myapp.com/pages/4
And each page belong to one user
What i need is to each user to set it's own custom name for the page.
I was thinking of using the friendly_id gem http://norman.github.com/friendly_id/
but I don't find any method to directly edit the slug to set a custom friendly url
how should i proceed?
FriendlyID is a great gem.
It shouldn't be hard to implement user defined page URL.
Create table pages with user_id and link
class User < ActiveRecord::Base
has_many :pages
class Page < ActiveRecord::Base
belongs_to :user
has_friendly_id :link # link is name of the column whose value will be replaced by slugged value
On the page#new you add an input for the link attribute.
Alternatively, you could set friendly_id on title or something else with :use_slug => true option. This way FriendlyID will take the title and modify it so it doesn't have and restricted characters. It will use it's own table to store slugs. Use cached_slug to increase performanse.
Updated
To give users a choice whether they wan't to set a custom link, you could do this:
Set friendly_id on the link field without slugs..
Make a virtual attribute permalink so you could show it in your forms.
In the before_filter, check whether the permalink is set.
If it is, write it to the link field.
If it's not, write title to the link field.
FriendlyID uses babosa gem to generate slugs. If you decide to use it as well, this is how your filter could look like:
protected
def generate_link
#you might need to use .nil? instead
self.link = self.permalink.empty? ? make_slug(self.title) : make_slug(self.permalink)
end
def make_slug(value)
value.to_slug.normalize.to_s #you could as well use ph6py's way
end
Adding to_param method to one of the models should help:
def to_param
"#{id}-#{call_to_method_that_returns_custom_name.parameterize}"
end
Hope this is what you are looking for :)
I am not using the friendly_url gem and am not sure whether my way is efficient. But it works fine for me.
I have a model called Node with id and friendly url field called url_title.
My routes.rb file:
resource 'nodes/:url_title', :to => 'Nodes#view'
nodes_controller.rb
class NodesController <ActiveController
def view
#node = Node.find_by_url_title(:params(url_title))
end
end
And use the #node variable to populate your view.
Now, whenever I type www.example.com/nodes/awesome-title , it takes me to the proper page. One argument against this can be need to create an index on a non-primary field. But I think that might be required for better performance even in the friendly_url gem. Also, the non-primary field url_title needs to be unique. Again, this might be required even for correct working for friendly_url .
Feel free to correct me if I am wrong in these assumptions.
There are a variety of ways, you can achieve this-
1) using Stringex
2) sluggable-finder
3) friendly_id
A complete step by step methodology with reasons for each to be used can be found out here. Happy reading!

In a migration: how to loop through Models and execute a private method in Model

I have a Post, to which a migration adds a new attribute and table column short_url. This attribute is either provided by the user, or, if left blank, automatically created:
class Post < ActiveRecord::Base
before_create :create_short_url
private
def create_short_url
if short_url.blank? || already_exists?(short_url)
write_attribute :short_url, random_string(6)
end
end
def random_string(length)
#innards are irrelevant for this question
end
end
In the migration, I want to run through all posts and have the short_url created and saved.
problem: Post.find(:all).each {|post| post.create_short_url} in the self.up is not possible, due to the private scope of the create_short_url method.
problem: Looping through posts and update!-ing them does not invoke the before_create :create_short_url, because it is not before create. Once migrated, I prefer to not have any before_update hooks in place: I don't need to change anything on update.
How would you tackle this? Copy over the random_string() and associated methods to the migration? Add specific migration helper methods to the Post?
Just use the Object method send (it doesn't check protected/private).
Post.all.each do |post|
post.send :create_short_url
post.save!
end
An alternative would be (but that could interfere with other migrations running in the same Ruby-process after that):
Post.before_save :create_short_url
Post.all.each(&:save!)
Visibility tip: Most of the time what you really mean is protected (see here). I recommend to use protected instead of private in this case.

Mapping domain.com/user/1 to domain.com/loginName

Has anyone tried before using subdomain-fu to change domain.com/user/1 to domain.com/UserName ?
Are there any other plugins to make this happen on rails 3?
You can do this by "overwriting" the to_param method in your User model.
Assuming you are using a User model, as you stated put some code like this in it:
#cleans up the url so it's purdy.
def to_param
loginName # you probably want login_name or just name, but whatever it's your code
end
So notice that you now have to change your User#show controller call though, since you really are doing a
User.find_by_name(params[:id]
Lastly, we should be able to change the routes so that the last route in your routes.rb file says:
match ':id' => 'users#show'
What you want is called pretty URLs. Take a look here: Pretty (dated) RESTful URLs in Rails

Rails 3 routing based on context

I am trying to implement a "context" system similar to the one used by GitHub. For example, a Post may be created belonging either to the User or one of the Companies the User belongs to depending on whether to User is in the "User" context or a context that refers to one of the Companies.
As a part of this, I'd like to be able to do routing based on the user's current context. For example, if the User is in their own context, /dashboard should route to users/show, but if they are in the context for Company with ID 35, then /dashboard should route to companies/35/dashboard.
I could route /dashboard to a special controller responsible for making such decisions, such as context#dashboard which could then do a redirect_to, but this doesn't feel quite right (perhaps because we're taking logic that the Rails routing module should be responsible for and moving it to a controller?)
What would be the proper way to solve this problem in Rails 3?
I finally found a solution to my problem that I like. This will use the URLs from my original question.
First, assume a session-stored Context object that stores whether the user is in a "user" context or a "company" context. If the user is in a "company" context, then the ID of the company they're working as is in the object as well. We can get the context via a helper named get_context and we can get the currently logged-in user via current_user.
Now, we set up our routes as so:
config/routes.rb:
MyApplication::Application.routes.draw do
get "dashboard" => "redirect", :user => "/users/show", :company => "/companies/:id/dashboard"
end
Now, app/controllers/redirect_controller.rb:
class RedirectController < ApplicationController
def method_missing(method, *args)
user_url = params[:user]
company_url = params[:company]
context = get_context
case context.type
when :user
redirect_to user_url.gsub(":id", current_user.id.to_s)
when :company
redirect_to company_url.gsub(":id", context.id.to_s)
end
end
end
It's easy enough to keep the actual URLs for the redirect where they belong (in the routes.rb file!) and that data is passed in to a DRY controller. I can even pass in the ID of the current context object in the route.
Your approach seems like the best way to me. Anything else would be more cluttered and not very standard.