Pass arguments to ruby function in this context - ruby-on-rails

disclaimer I am pretty new to Ruby / Rails
I am using RPH::Navigation for creating my navigation menus. What I am trying to do now is add some sub-nav items and I am doing so like this:
suboptions.each do |group|
def relpath
link(group[:link], group[:args]).
end
sub_menu.item group[:name], :path => :relpath
end
link is just a method that returns "#{link}?#{query_string}" where query_string is built from the args hash.
What I was originally trying to do was something like
sub_menu.item group[:name], :path => link(group[:link], group[:args])
but that put the return value in, and later it is called, returning a method not found error.
My current approach has the problem that group is not in scope within relpath. I tried also using Proc.new and lambda, but since those are not called like normal functions it chokes on them as well.
What can I do to correct this? What is the proper way?
EDIT
If I do
suboptions.each do |group|
def relpath(group)
link(group[:link], group[:args]).
end
sub_menu.item group[:name], :path => relpath(group)
end
Then the error I get is:
undefined method `mylink?myarg=1' for #<#<Class:0x007fd8068acd58>:0x007fd8068b07f0>
EDIT 2
Here is more extensive example of the code.
menu.item MyTestsController, :text => 'Test', :if => Proc.new { |view| view.can? :read, MyTest } do |sub_menu|
suboptions = [{:link => "tests", :name => "All Systems", :args => {:system_id => 0}},
{:link => "tests", :name => "System A", :args => {:system_id => 1}}]
suboptions.each do |group|
def relpath(group)
link(group[:link], group[:args]).
end
sub_menu.item group[:name], :path => relpath(group)
end
end

I have no experience with the navigation library in question, but looking at the documentation at https://github.com/rpheath/navigation it seems you are expected to give the name of a Rails named route helper as the path argument - not an actual URL route. The "undefined method" is simply generated because RPH::Navigation tries to call a helper method by the name that is defined in :path argument, and in this case Rails cannot find a named route helper method called "mylink?myarg=1". So basically, what you would need to do is to create a named route, and use that as the path.

Related

Multilingual routes - Advanced constraints not taken into consideration when paths are generated by url_for

I have a multilingual site in rails 3.2 that has some language-specific routes that map to the same action. Like:
For mydomain.fr
match "/bonjour_monde" => 'foo#bar'
For mydomain.de
match "/hallo_welt" => 'foo#bar'
To solve this I have used an advanced constraint when declaring the routes:
Application.routes.draw do
constraints(SiteInitializer.for_country("fr")) do
match "/bonjour_monde" => 'foo#bar'
end
constraints(SiteInitializer.for_country("de")) do
match "/hallo_welt" => 'foo#bar'
end
end
Where the SiteInitializer is just a class which responds to the matches? method and determines if the request is for the correct domain. This is actually only pseudo-code just demonstrating my setup.
class SiteInitializer
def initialize(country_code)
#country_code = country_code
end
def self.for_country(country_code)
new(country_code)
end
def matches?(request)
# based on the request, decide if this route should be declared
decide_which_country_code_from_request(request) == #country_code
end
end
This works fine. When requesting mysite.fr/bonjour_monde the app dispatches correctly and the paths are only bound to their specific domain.
mysite.fr/bonjour_monde => HTTP 200
mysite.fr/hallo_welt => HTTP 404
mysite.de/bonjour_monde => HTTP 404
mysite.de/hallo_welt => HTTP 200
Now, all is fine unless you start using things like url_for(:controller => 'foo', :action => 'bar'). If you do this, the constraints are not taken into consideration. This results in that the generated path from rails (Journey class), will be arbitrary.
If I somewhere in any view use url_for, like
url_for(:controller => 'foo', :action => 'bar')
rails will choose any arbitrary declared route that matches the controller action, probably the first one declared, skipping to check any advanced constraint.
If the user visits mysite.de/hallo_welt, and the view does something like:
= url_for(:controller => 'foo', :action => 'bar', :page => '2')
The output might be
mysite.de/bonjour_monde?page=2
^
wrong language
Actually, I don't use url_for specifically in code, but some gems like kaminari (paginator) do this and that's why you might be limited in using libraries that use the standard helper methods when generating paths.
Now, I'm not so sure if the Journey class should take the request context into consideration. But how would you approach this kind of problem?

Generate the URL for a generic path with possible options in Rails

I'm creating a customisable nav menu for our site and have run into the following problem.
I need to generate a URL to any controller and action on the site and optionally pass it parameters. I was able to do the former by simply saying:
url_for(:controller => nav[:controller_name], :action => nav[:action_name])
which is great for sending you to {controller}/{action}. eg. news/articles
Throwing options in suddenly changes the game. Now I need to send you to something like:
{controller}/{action}/{category}/{slug}/{id}
eg. news/articles/world-domination/montana-max-vows-revenge/12345
the helper for the above would be something along the lines of:
news_article_path('world-domination', 'montana-max-vows-revenge', '12345')
and I haven't been able to replicate that in a vanilla url_for due to the arguments.
What I have done, and I don't really like is:
url_for(send("#{nav[:controller_name]}_#{nav[:action_name]}_path", *nav[:options]))
which generates the helper using send and then passes it a kwargs list. I'm sure there's a better way to do that surely?
You can do this cleanly if you are able to name the options (split here over lines for legibility):
url_for({
:controller => nav[:controller_name],
:action => nav[:action_name]
}.merge(nav[:options] || {}))
where
nav = {
:controller_name => 'news',
:action_name => 'articles',
:options => {
:category => 'world-domination',
:slug => 'montana-max-vows-revenge',
:id => '12345'
}
}

How to write a dynamic route in Rails 3?

I'm trying to write a generic route that will allow me to refer to it by the controller's action.
I'm using the following line:
match ':action' => 'pages#:action', :as => 'action'
let's say an action named `foobar' in the pages controller. I'd like to be able to write
link_to 'Click Me', pages_foobar_path
in a view. The problem is that I get the error Invalid route name: ':action' when I try to write that route.
Mind you, the line
match ':action' => 'pages#:action'
without the :as parameter works perfectly, but then I have to manually write the path, as such:
link_to 'Click Me', '/pages/foobar'
any way around that?
If dynamic means "recognize my actions when Rails starts up and generate routes dynamically":
It's not something that I would do, but it does what you want it to do without any redirection nor method_missing runtime overhead.
In config/routes.rb
controller_filenames = Dir.new("#{Rails.root}/app/controllers").entries
controller_filenames.each do |filename|
# you could restrict to only the pages_controller.rb on the next line,
# and in that case, you could simplify even more (see next example)...
if filename =~ /_controller\.rb$/
controller_name = filename.sub(/.rb$/, "")
controller_route_name = controller_name.sub(/_controller$/, "")
controller = controller_name.camelize.constantize.new
controller.action_methods.each do |action|
# if you don't want the controller name in your path match, just omit it...
match "#{controller_route_name}/#{action}" => "#{controller_route_name}##{action}", :as => "#{controller_route_name}_#{action}"
end
end
end
If you only want to do this for your pages_controller.rb file, then:
controller_name = "pages_controller"
controller_route_name = "pages"
controller = controller_name.camelize.constantize.new
controller.action_methods.each do |action|
# I've removed the controller_route_name from the match here...
match "#{action}" => "#{controller_route_name}##{action}", :as => "#{controller_route_name}_#{action}"
end
Now, if dynamic means "generate a route whenever I dynamically generate a new action":
You could really play with fire. Any of your existing actions can define new actions and routes. For example, I could define a route in config/routes.rb (but this could be any existing route):
match '/dynamic_define' => 'application#dynamic_define'
Couple that with a method in ApplicationController (again, this could be any existing action):
def dynamic_define
method_name = params[:mname]
self.class.send(:define_method, method_name) {
render :text => "output from #{method_name}"
}
Rails.application.routes.disable_clear_and_finalize = true
Rails.application.routes.draw do
match "/#{method_name}" => "application##{method_name}", :as => "application_#{method_name}"
end
render :text => "dynamic_define just created a new action named #{method_name}"
end
In your browser, you can visit:
/dynamic_define?mname=my_new_dynamic_action
# browser renders "dynamic_define just created a new action named my_new_dynamic_action"
And then visit:
/my_new_dynamic_action
# browser renders "output from my_new_dynamic_action"
I think you can get as far as:
link_to 'Click me', pages_path(:action)
by redirecting
match ':action' => 'pages#:action'
match '/pages/:action' => redirect(":action") # pages_path(:action) will match
This is less typing than the approach suggested in the first answer, but seems less expressive if anything.
I suppose you could override method_missing in your view class to catch pages_[stuff]_path and generate the proper string, e.g.
def method_missing(name, *args, &block)
if name =~ /^pages_[a-z]*_path$/
"/#{name.to_s.gsub!(/^pages_/,'').gsub!(/_path$/,'')}"
else
super
end
end
Forgive me if my method_missing knowledge or regex capabilities are lacking - hopefully this is helpful directionally at least.
If you write your route like that, the right way to access it is:
link_to 'Click me', action_path(:action => 'foobar')

Creating a named route for a controller with static actions using Rails 3 routing API

I have a basic controller with named actions representing static pages:
class YourTripController < ApplicationController
def when_to_visit
end
def booking_a_trip
end
...
end
I want a named route to access these actions like:
your_trip_path(:when_to_visit)
your_trip_path(:booking_a_trip)
Seems simple enough right? But I also want to have the URLs use dashs rather then underscores for the generated paths:
example.com/your-trip/when-to-visit
example.com/your-trip/booking-a-trip
Given these constraints what does my route/s look like?
I've tried many approaches and this is the closest I've got so far:
controller :your_trip, :path => 'your-trip', :as => :your_trip do
get "when_to_visit", :path => 'when-to-visit'
get "booking_a_trip", :path => 'booking-a-trip'
end
This correctly routes me to the right action but using a url helper such as your_trip_path(:when_to_visit) results in:
No route matches {:action=>"when_to_visit", :controller=>"your_trip"}
Match url to controller#action and specify route name with :as:
get "your-trip/when-to-visit" => "your_trip#when_to_visit", :as => "when_to_visit"
Url helpers work too:
rails c
Loading development environment (Rails 3.0.3)
ruby-1.8.7-p302 :001 > app.when_to_visit_path
=> "/your-trip/when-to-visit"
Looks like with the help of routing-filter gem it might be possible write a custom route filter for hyphenated urls. That's of course an overkill for simple cases with just a couple of static pages.
Your version with tiny modification works:
controller :your_trip, :path => 'your-trip', :as => :your_trip do
get "when_to_visit", :path => 'when-to-visit', :as => :when_to_visit
get "booking_a_trip", :path => 'booking-a-trip', :as => :booking_a_trip
end
Routes are then named as:
your_trip_when_to_visit
your_trip_booking_a_trip

rails3 routes issue

I have following routes.
pota.resources :on_k,
:as => ':klass',
:path_prefix => 'pota/klass',
:controller => 'main'
When I do rake routes this is what I get for show method:
pota_on_k GET /pota/klass/:klass/:id(.:format)
{:action=>"show", :controller=>"pota/main"}
Above code works fine in rails 2.x . However if I am using rails3 then I get following error
ActionController::RoutingError: No route matches
{:action=>"show", :controller=>"pota/main", :klass=>"vehicle/door", :id=>1}
Notice that I am passing 'vehicle/door' as :klass. If I pass a standard model like :klass => 'pet' then it works fine. However if I pass a nested model name like :klass => 'vehicle/door' then I get route error in rails3.
I guess that is because I have '/' in the value . I can solve that by having a regex but I might also pass :klass which is not nested.
On a class like Vehicle::Car I do
Vehicle::Car.underscore #=> vehicle/car
"vehicle/car".camelize.constantize #=> Vehicle::Car
This underscore and camelize/constantize on the other side makes it easier to pass nested class name.
Any idea on how to go about fixing it for rails3?
STOP!
Think about what you're doing here - you should not be calling constantize on url parameters. Assuming that you're likely to be calling find on the result you're basically giving a hacker a way to query every ActiveRecord model in your application.
A better way is to use meta programming to dynamically build static routes that can't be hacked, e.g:
%w[pet vehicle/car vehicle/bike].each do |klass|
resources :pota,
:path => "pota/#{klass.pluralize}",
:as => "pota_#{klass.tr('/','_').pluralize}",
:defaults => { :klass => klass }
end
Then you can add a helper method that calls the appropriate named route helper to generate urls based upon a passed model instance.

Resources