I'm trying to achieve full internationalization of my routes in a Rails3.1 app. I'm already using Francesc Pla's rails-translate-routes to localize route actions and resources. The last step is to be able to translate slugs for some of my models.
Route to be translated:
http://myhost.com/products/pharmacy --> http://myhost.com/productos/farmacia
I have a nested route of the form
# routes.rb
match 'products/:category_slug' => "products#index"
I have a model Category with an instance #<Category id: 1, slug: "pharmacy"> and I do find_by_slug category in my ProductsController.
Any ideas on how to do translate the :category_slug part of the route?
As far as I'm aware, you can call translation helpers directly from your controller as long as you namespace correctly with I18n.
So your ProductsController could contain something like the following:
class ProductsController < ApplicationController
def index
i18n_slug = I18n.t("locale.category.#{params[:category_slug]}")
#category = Category.find_by_slug(i18n_slug)
end
end
You should probably inform yourself as to potential security risks of passing the params directly into the translation engine, though I'm not aware of any. You might also consider
moving that into a before filter or into the application controller if it will be used in multiple controller actions.
Related
I am working on a Rails app, and I am looking for a way to route to different actions in the controller based on the existence of parameters in the url.
For example I want website.com/model to route to model#index, however I want website.com/model?opt=dev to route to model#show. Is there some way this can be done?
Use route constraints to look at the request object and see if it has URL parameters. If you're using restful routes, you want to put this "one-off" before the restful route. Something like this:
get 'users' => 'users#show', constraints: { query_string: /.+/ }
resources :users
So what this is saying is that if you request "/users?opt=dev" then it will match your special case. Otherwise, it falls through to your normal restful route to the index action. Your model#show action will then have to know to pick up the param[:opt] and do whatever with it.
Also, note that the regex is very loose and it's simply checking for ANY param...you'll want to tighten that up to fit whatever you're trying to do.
Not strictly the same, but if you came to this post and were wondering how to do the same via a POST, then you can do it based on the request_paramters.
for your routes.rb ..
module MyConstraintName
extend self
def matches?(request)
request.request_parameters["routeFlag"] == "routeToModelShow"
end
end
match "pages/:id", :via=>:post, :controller=>"model", :action=>"show", :constraints => MyConstraintName
and in your form for example..
<%= hidden_field_tag :routeFlag, "routeToModelShow" %>
Still pretty new to Rails, so if I'm taking the completely wrong approach, please feel free to straiten me out.
How do I make routes.rb aware that there's a root controller full of actions that don't manipulate models, while preserving the route helper methods? I'd like it to respond to requests like these:
http://localhost/download
http://localhost/share
With route helpers like
download_app_path
share_path
but without static named routes like these:
match '/download' => 'site#download', :as => :download_app
match '/share' => 'site#share', :as => :share
from a SiteController that doesn't create, show, or otherwise manipulates models from my app.
I've tried using an approach like this, but it works without generating the route helpers ( naturally )
match '/:action', :controller => 'site'
I could theoretically do without the route helpers, but I think they're a bit easier to read than passing hashes of url options to link_to or form methods.
Is there a way to accomplish a more resourceful root controller, or is what I'm trying to do unconventional for Rails?
Edit
For clarity, here's what this SiteController class looks like:
class SiteController < ApplicationController
def download
#custom_options = { .. }
end
def share
#custom_options = { .. }
end
def about
end
end
Its purpose is to allow me to collect pages that don't interact with resources ( such as Users or Friendships ) into a single controller and maintain them all in one place. I'm trying to set this controller up as the application root controller - so all paths from this controller will be directly off the app host ( myapp.com/download )
Thanks in advance!
routes and resources are not tied to models. it's just a RESTful convention. if you just want to use the index actions, in your example download and share could be done like
resouce :download, only: [:index]
resouce :share, only: [:index]
see all the examples in the guides http://guides.rubyonrails.org/routing.html
if you want to add the download and share functionality to some "resource" like, say a picture, then you would do something like:
resources :pictures do
get 'download', :on => :member
get 'share', :on => :member
end
a resource always has and endpoint /pictures for example. so if you want to have paths directly to your host, then you need to provide custom matchers in your routes like you did in your examples.
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
...
I'm having a problem in my Rails 3.2 app where a virtual attribute sent restfully via JSON is not in the right place in the params hash. Well, it isn't where I expect. It remains to be seen if my expectations are correct. :)
I have a model using the standard virtual attribute pattern, like this:
class Track < ActiveRecord::Base
def rating
# get logic removed for brevity
end
def rating=(value)
# set logic
end
def as_json(options={}) # so my method is in the JSON when I use respond_with/to_json
super(options.merge(methods: [:rating]))
end
end
The JSON sent to my controller looks like this:
{"id":1,"name":"Icarus - Main Theme 2","rating":2}
To be clear, name and id are not virtual, rating is.
I end up with this in the params hash, after rails does its magic:
{"id"=>"1", "name"=>"Icarus - Main Theme 2", "rating"=>2, "track"=>{"id"=>"1", "name"=>"Icarus - Main Theme 2"}}
As you can see, id and name make it to the nested :track hash, but rating does not. Is this expected behavior? It breaks the (somewhat) standard practice of using the nested hash in the controller because the nested hash does not contain all the parameters I need.
Track.update(params[:id], params[:track]) # :track is missing rating
Thanks for your help!
I recently ran into this gotcha as well. The problem is, the params wrapper is looking at your model Track.attribute_names to determine how to map the data into a :track => {params} hash. If you don't have a model associated, the default will be to wrap the params based on the controller name, and include all of the values:
class SinglesController < ApplicationController
def create
#params[:single] will contain all of your attributes as it doesn't
# have an activerecord model to look at.
#track_single = Track.new(params[:single])
end
end
You can call wrap_parameters in your controller to tell action controller what attributes to include when its wrapping your params, like so:
class TracksController < ApplicationController
wrap_parameters :track, :include => :rating
#other controller stuff below
end
See more here: http://api.rubyonrails.org/classes/ActionController/ParamsWrapper.html
Maybe if you assign the rating virtual attribute inside the nested hash like this:
def as_json(options={})
super(options.merge(:track => {:methods => #rating}))
end
It would behave the way you expected.
Just ran across this problem and figured out a pretty decent solution. Add the following to your ApplicationController
wrap_parameters exclude: [:controller, :action, :format] + ActionController::ParamsWrapper::EXCLUDE_PARAMETERS
This way, everything is nested under your resource (except for stuff Rails adds to the params hash) and you won't ever have to append to a controller specific call of wrap_parameters again. :D
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.