How to set defaults for parameters of url helper methods? - ruby-on-rails

I use language code as a prefix, e.g. www.mydomain.com/en/posts/1.
This is what I did in routes.rb:
scope ":lang" do
resources :posts
end
Now I can easily use url helpers such as: post_path(post.id, :lang => :en). The problem is that I would like to use a value in a cookie as a default language. So I could write just post_path(post.id).
Is there any way how to set default values for parameters in url helpers? I can't find the source code of url helpers - can someone point me in the right direction?
Another way: I have already tried to set it in routes.rb but it's evaluated in startup time only, this does not work for me:
scope ":lang", :defaults => { :lang => lambda { "en" } } do
resources :posts
end

Ryan Bates covered this in todays railscast: http://railscasts.com/episodes/138-i18n-revised
You find the source for url_for here: http://api.rubyonrails.org/classes/ActionDispatch/Routing/UrlFor.html
You will see it merges the given options with url_options, which in turn calls default_url_options.
Add the following as private methods to your application_controller.rb and you should be set.
def locale_from_cookie
# retrieve the locale
end
def default_url_options(options = {})
{:lang => locale_from_cookie}
end

doesterr below has almost got it. That version of default_url_options won't play nice with others. You want to augment instead of clobber options passed in:
def locale_from_cookie
# retrieve the locale
end
def default_url_options(options = {})
options.merge(:lang => locale_from_cookie)
end

This is coding from my head, so no guarantee, but give this a try in an initializer:
module MyRoutingStuff
alias :original_url_for :url_for
def url_for(options = {})
options[:lang] = :en unless options[:lang] # whatever code you want to set your default
original_url_for
end
end
ActionDispatch::Routing::UrlFor.send(:include, MyRoutingStuff)
or straight monkey-patch...
module ActionDispatch
module Routing
module UrlFor
alias :original_url_for :url_for
def url_for(options = {})
options[:lang] = :en unless options[:lang] # whatever code you want to set your default
original_url_for
end
end
end
end
The code for url_for is in actionpack/lib/routing/url_for.rb in Rails 3.0.7

Related

How to extend url_for to provide dynamic default parameters

I'm trying to figure out a clean and dry way to extend the url_for such that it provides a dynamically based default parameter. I know that sounds weird, let me explain:
I have a set of nested resource routes that sit beneath a dynamic scope:
scope "/:network", constraints: {:network => /[^\/]+/} do
constraints DomainConstraint.new do
resources :users do
resources :posts
end
end
end
This gives routes like:
/mysite.com/users/mike
/someothersite.com/users/sally
In order to generate these routes in a view, I can easily do:
mike = User.find_by_name("mike")
sally = User.find_by_name("sally")
user_path(mike.network, mike)
user_path(sally.network, sally)
However, this does not seem very DRY to me as the network is fixed for each user. I'd rather be able to concisely say:
user_path(mike)
So, I've come up with a solution that works, but it seems very hacky and I'm wondering if there is a more "proper" way to do it:
module UrlForWithDefault
def self.included(base)
base.module_eval do
alias_method_chain :url_for, :default
end
end
def url_for_with_default(*args)
if args[0].kind_of?(Hash)
opts = args[0]
if opts.has_key?(:_positional_keys) and opts[:_positional_keys].include?(:network)
opts[:_positional_args].unshift(opts[:_positional_args][0].network)
url_for_without_default(opts)
end
end
url_for_without_default(*args)
end
end
ActionDispatch::Routing::UrlFor.send(:include, UrlForWithDefault)
I don't like this approach because it hacks the ":_positional_keys" and ":_positional_args" attributes, which could potentially change.
could not url_options or Rails.application.routes.default_url_options do what you want?

How to use url helpers in lib modules, and set host for multiple environments

In a Rails 3.2 app I need to access url_helpers in a lib file. I'm using
Rails.application.routes.url_helpers.model_url(model)
but I'm getting
ArgumentError (Missing host to link to! Please provide the :host parameter, set default_url_options[:host], or set :only_path to true):
I've found a few things written about this, but nothing that really explains how to solve this for multiple environments.
i.e. I assume I need to add something to my development.rb and production.rb files, but what?
Closest I've seen to an answer suggested using config.action_mailer.default_url_option, but this does not work outside of action mailer.
What is the correct way to set the host for multiple environments?
This is a problem that I keep running into and has bugged me for a while.
I know many will say it goes against the MVC architecture to access url_helpers in models and modules, but there are times—such as when interfacing with an external API—where it does make sense.
After much searching I've found an answer!
#lib/routing.rb
module Routing
extend ActiveSupport::Concern
include Rails.application.routes.url_helpers
included do
def default_url_options
ActionMailer::Base.default_url_options
end
end
end
#lib/url_generator.rb
class UrlGenerator
include Routing
end
I can now call the following in any model, module, class, console, etc
UrlGenerator.new.models_url
Result!
A slight improvement (at least for me) on Andy's lovely answer
module UrlHelpers
extend ActiveSupport::Concern
class Base
include Rails.application.routes.url_helpers
def default_url_options
ActionMailer::Base.default_url_options
end
end
def url_helpers
#url_helpers ||= UrlHelpers::Base.new
end
def self.method_missing method, *args, &block
#url_helpers ||= UrlHelpers::Base.new
if #url_helpers.respond_to?(method)
#url_helpers.send(method, *args, &block)
else
super method, *args, &block
end
end
end
and the way you use it is:
include UrlHelpers
url_helpers.posts_url # returns https://blabla.com/posts
or simply
UrlHelpers.posts_url # returns https://blabla.com/posts
Thank you Andy! +1
Use this string in any module controller to get application URL-helpers works in any view or controller.
include Rails.application.routes.url_helpers
Please note, some internal module url-helpers should be namespaced.
Example:
root application
routes.rb
Rails.application.routes.draw do
get 'action' => "contr#action", :as => 'welcome'
mount Eb::Core::Engine => "/" , :as => 'eb'
end
Url helpers in module Eb:
users_path
Add include Rails.application.routes.url_helpers in controller contr
So after that helper should be
eb.users_path
So inside Eb module you can use welcome_path same as in root application!
Not sure if this works in Rails 3.2, but in later versions setting default url options for the routes can be done directly on the routes instance.
So for example, to set the same options as for ActionMailer:
Rails.application.routes.default_url_options = ActionMailer::Base.default_url_options

Dynamic namespaced controllers w/ fallback in Rails

I have a somewhat bizarre requirement for a new Rails application. I need to build an application in which all routes are defined in multiple namespaces (let me explain). I want to have an application in which school subjects (math, english, etc) are the namespaces:
%w[math english].each do |subject|
namespace subject.to_sym do
resources :students
end
end
This is great and it works but it requires me to create a namespaced StudentsController for each subject which means if I add a new subject then I need to create a new controller.
What I would like is to create a Base::StudentsController and if, let's say the Math::StudentsController exists then it will be used and if it doesn't exist, then we can dynamically create this controller and inherit from Base::StudentsController.
Is this something that is possible? If so then how would I go about implementing this?
With routes defined this way:
%w[math english].each do |subject|
scope "/#{subject}" do
begin
"#{subject.camelcase}::StudentsController".constantize
resources :students, controller: "#{subject}::students", only: :index
rescue
resources :students, controller: "base::students", only: :index
end
end
end
rake routes outputs:
students GET /math/students(.:format) base::students#index
GET /english/students(.:format) english::students#index
if english/students_controller.rb is present and math/students_controller. is absent.
To restate your requirements:
Minimal declarations per subject/resource pair
Use dedicated controller (Math::StudentsController) if it exists, otherwise use base controller (StudentsController)
Rails expects each route to have a dedicated controller, and doesn't really have a good way to support the second requirement. So, this is how I would do it:
Dynamicroutes::Application.routes.draw do
SUBJECTS = [ "math", "english", "chemistry" ]
RESOURCES = [ "assignments", "students" ]
class DedicatedSubjectResourceControllerConstraint
def initialize(subject, resource)
#subject = subject
#resource = resource
end
def matches?(request)
begin
defined?("#{#subject.capitalize}::#{#resource.capitalize}")
return true
rescue NameError
Rails.logger.debug "No such class: #{#subject.capitalize}::#{#resource.capitalize}"
return false
end
end
end
class ValidSubjectConstraint
def matches?(request)
return SUBJECTS.include?(request.path_parameters[:subject])
end
end
SUBJECTS.each do |subject|
RESOURCES.each do |resource|
namespace subject, :constraints => DedicatedSubjectResourceControllerConstraint.new(subject, resource) do
resources resource
end
end
end
RESOURCES.each do |resource|
scope "/:subject", :constraints => ValidSubjectConstraint.new do
resources resource
end
end
end
This sounds like a use for const_missing. If what you want to do is
to create a Base::StudentsController
and if, let's say the Math::StudentsController exists
then it will be used
and if it doesn't exist, then we can dynamically create this controller and inherit from Base::StudentsController
You can achieve that by adding dynamic constant lookup (const_missing) and dynamic constant definition with inheritance (Object.const_set).
I imagine something like this; with a few tweaks and more rigorous checking, would work:
# initializers/dynamic_controllers.rb
class ActionDispatch::Routing::RouteSet
SUBJECTS = [ "math", "english", "chemistry" ]
def const_missing(name, *args, &block)
if SUBJECTS.any?{ |subject| name.include? subject.uppercase }
Object.const_set name, Class.new(Base::StudentsController)
else
super
end
end
end
That'll add dynamic constant lookups to ActionDispatch::Routing::RouteSet, from which Dynamicroutes::Application.routes inherits, so undefined constants in Dynamicroutes::Application.routes.draw will generate the corresponding classes subclassed from Base::StudentsController.
I believe this will do it:
%w[math english].each do |subject|
namespace subject.to_sym do
resources :students
end
end
match ':subject/students(/:action(/:id))' => 'base/students'
With these combined routes, /math/students goes to the Math::StudentsController, /english/students/ goes to the English::StudentsController, and all other subjects (e.g. /physics/students and /cs/students) go to the Base::StudentsController.
Which I think is exactly what you want and only adds one line of code to your original solution.
All the routing helpers like resources, scope, etc are just functions inside your application's routes. You could just define a custom function as follows:
YourApplication.routes.draw do
# Let's define a custom method that you're going to use for your specific needs
def resources_with_fallback(*args, &block)
target_module = #scope[:module].camelize.constantize
target_controller = "#{args.first.to_s}_controller".camelize
fallback_controller = args.last.delete(:fallback).to_s.camelize.constantize
# Create the target controller class
# using fallback_controller as the superclass
# if it doesn't exist
unless target_module.const_defined?(target_controller)
target_module.const_set target_controller, Class.new(fallback_controller)
end
# Call original resources method
resources *args, &block
end
# Now go ahead and define your routes!
namespace "test" do
namespace "new" do
# Use our custom_resources function and pass a fallback parameter
custom_resources :photos, :fallback => 'base/some_controller'
end
end
end
I tested this in Rails 3.2, but it should equally work well in all 3.x versions.
I included no null checks or begin/rescue blocks anywhere. Since you're going to use this custom function only when required, I'm assuming that you will pass the correct and necessary parameters. If say you passed a fallback controller that doesn't exist, I'd rather that the routes parsing fail with an exception, rather than trying to handle it.
Edit: Typo in function arguments
Edit 2: Forgot &block in function arguments
Edit 3: Add "_controller" to the target_controller variable
I ended up writing some custom logic into ActionDispatch::Routing::RouteSet::Dispatcher.controller_reference. I attempt to look up all of the constants required for the given controller and create them if they're missing. This code is FAR from perfect so please feel free to edit w/ improvements.
class ActionDispatch::Routing::RouteSet::Dispatcher
private
def controller_reference(controller_param)
const_name = #controller_class_names[controller_param] ||= "#{controller_param.camelize}Controller"
obj = Object
const_name.split('::').each do |cn|
begin
obj = obj.const_get(cn)
rescue
if obj == Object
obj = obj.const_set(cn, Class.new(ApplicationController))
else
puts "Creating #{obj}::#{cn} based on Generic::#{cn}"
obj = obj.const_set(cn, Class.new("Generic::#{cn}".constantize))
end
end
end
ActiveSupport::Dependencies.constantize(const_name)
end
end

Limiting download_links for ActiveAdmin based on AdminUser model

I am trying to limit who can access the csv/json/... exports in ActiveAdmin based on the field 'limited'. I'd like to a) hide the links and b) return nothing at all if the path were to get hit anyway
I tried the following:
index downloads_links: !current_admin_user.limited? do
# ...
end
as well as
csv do
return if current_admin_user.limited?
# ...
end
I also briefly tried using procs and lambda's but that's probably not the solution here either?
Neither appear to work and are giving me nomethoderrors on ActiveAdmin::DSLResource and ActiveAdmin::CSVBuilder respectively
Any tips are welcome, thank you
i was able to achieve this with a simple monkey patch, but i was using cancan. cancan helper method 'can?' worked fine, but i wasn't testing the 'current_admin_user'. please, try it
module ActiveAdmin
module Views
class PaginatedCollection
def build_download_format_links(formats = self.class.formats)
params = request.query_parameters.except :format, :commit
links = formats.map { |format| link_to format.to_s.upcase, params: params, format: format }
unless current_admin_user.limited?
div :class => "download_links" do
text_node [I18n.t('active_admin.download'), links].flatten.join(" ").html_safe
end
end
end
end
end
end
upd:
i've tried with current_admin_user, and it worked.
also if you need to limit the formats, you can redefine formats method it this module, using your 'limited' method:
module ActiveAdmin
module Views
class PaginatedCollection
def formats
if current_admin_user.limited?
#formats ||= [:csv] # anything you need for limited users
else
#formats ||= [:csv, :xml, :json]
end
#formats.clone
end
end
end
end

rails-breadcrumb and I18n

I run into an issue with rails-breadcrumb since i localized my application.
In my Controller, I've got this :
class FooController < PrivateController
add_breadcrumb I18n.t('breadcrumbs.foo.index'), :foo_url
end
When my breadcrumb is displayed, the localized string is always taken from en.yml, no matter which language i set up in I18n.locale
After having look at the code, it occurs that add_breadcrumbacts as a before_filter, and after some test, i came to the conclusion that, even if the content of add_breadcrumb has the right locale, it seems that the value passed does not.
If I try this :
add_breadcrumb I18n.t('breadcrumbs.foo.index', :locale => "fr"), :foo_url
Everything goes fine.
How cas i force my string to be correctly localized?
Thank you per advance
I finaly got this. After i determined that my issue cames out of the fact that I18n didn't know anything about my locale as i was asking it to translate something, i monkey-patched the rails-breadcrumb to manage the localization itself.
Know i pass a Symbol as first parameters, and i call I18n.translate() in rails-breadcrumb
add_breadcrumb (:'breadcrumbs.foo.index'), :foo_url
d
# config/initializers/rails-breadcrumb-fix.rb
module Rails
module Breadcrumbs
class ActionController::Base
protected
def add_breadcrumb(name, url = '')
#breadcrumbs ||= []
# if given `name` is a Symbol, we localize it
if name.is_a?(Symbol)
name = I18n.t(name)
end
url = send(url) if url.is_a?(Symbol)
#breadcrumbs << [name, url]
end
def self.add_breadcrumb(name, url, options = {})
before_filter options do |controller|
controller.send(:add_breadcrumb, name, url)
end
end
end
module Helper
def breadcrumbs(separator = "›")
#breadcrumbs.map do |txt, path|
link_to_unless (path.blank? || current_page?(path)), h(txt), path
end.join(" #{separator} ").html_safe
end
end
end
end
ActionController::Base.send(:include, Rails::Breadcrumbs)
ActionView::Base.send(:include, Rails::Breadcrumbs::Helper)

Resources