How to extend url_for to provide dynamic default parameters - ruby-on-rails

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?

Related

Do not define constants this way within a block. When defining class within a block

Im defining a class within the routes block to handle ajax stuff
#config/routes.rb
Rails.application.routes.draw do
class OnlyAjaxRequest
def matches?(request)
request.xhr?
end
end
#rest of the routes
end
However I get this warning in the editor:
Do not define constants this way within a block.Lint/ConstantDefinitionInBlock(RuboCop)
it seems this is a bad practice, but Im not sure how I can fix it, any ideas?
From the docs:
Do not define constants within a block, since the block’s scope does not isolate or namespace the constant in any way.
If you are trying to define that constant once, define it outside of the block instead, or use a variable or method if defining the constant in the outer scope would be problematic.
You can define the OnlyAjaxRequest class outside the block to fix the issue.
class OnlyAjaxRequest
def matches?(request)
request.xhr?
end
end
Rails.application.routes.draw do
# ...
end
Since the constraint is really simple, you don't even need to create a class. You can pass a lambda to the constraints option.
is_ajax_request = ->(request) { request.xhr? }
get '/some_route', to: 'test#action', constraints: is_ajax_request
I don't see why you want to define a class inside the block in your concrete case, since you can it define equally well outsie, but in case you ever come accross a situation where you really want to do it locally, you can do it as
Object.const_set(
"OnlyAjayRequest",
Class.new {
def matches?(request)
request.xhr?
end
}
)

Change default sort order in ActiveAdmin scope

For most models, the default sort order (id desc) is fine. But for a couple of scopes on one of my models, it would make more sense to reverse the order, or order by the updated_at field.
I seem unable to achieve this without breaking other functionality, so I'm hoping someone else might teach me how to do this!
I've tried adding a .order() on the objects I'm returning:
scope :example do |models|
models.order('id asc')
end
This does not seem to have any effect.
I've also tried unscoping, which kind of works. It does sort my objects the way I want to, but it completely breaks all filtering/search functionality.
scope :example do |models|
models.unscoped.order('id asc')
end
What to do?
use config.sort_order like:
config.sort_order = "updated_at_desc"
Why not make 2 scopes in your model, one for a particular sort order, the other, reversed, then in ActiveAdmin set one as the default?
scope :example_asc, :default => true
scope :example_desc
If that doesn't work for you, perhaps create a controller block in ActiveAdmin that defines what you're trying to do:
controller do
def asc
Model.order('id ASC')
end
def desc
Model.order('id DESC')
end
end
scope :example do |models|
asc
end
scope :example do |models|
desc
end
And finally, I think this answer is probably fairly accurate: https://stackoverflow.com/a/17612718/175825
But you might want to investigate more about how you can implement sort_order:
https://github.com/gregbell/active_admin/blob/master/lib/active_admin/dsl.rb#L25
https://github.com/gregbell/active_admin/blob/master/lib/active_admin/dsl.rb#L97
For what it's worth, I'm not a fan of ActiveAdmin's skinny docs. Good luck.
This works and also ensures that the default sort order to correctly reflected in the UI.
This is overwriting the apply_sorting method of ActiveAdmin, so all the usual caveats about monkey patching third party gems apply.
module ActiveAdmin
class ResourceController < BaseController
module CallableSortOrder
def apply_sorting(chain)
params[:order] ||= if active_admin_config.sort_order&.respond_to?(:call)
active_admin_config.sort_order.call(self)
else
active_admin_config.sort_order
end
super
end
end
prepend CallableSortOrder
end
end
Use it like this:
config.sort_order = ->(controller) {
controller.params[:scope] == 'something' ? 'created_at_desc' : 'name_asc'
}
You need to use reorder to override the default order, but also be sensitive as to whether ActiveAdmin sorts are being applied
scope :example do |models|
if params[:order].blank? or params[:order] == "id_desc" #default ordering
models.example.reorder('your order SQL here')
else
models.example
end
end
#yxf's answer is correct, but I found it unclear ("where does config come from?!").
Docs: https://activeadmin.info/3-index-pages.html (just search "order")
Code:
ActiveAdmin.register Post do
config.sort_order = 'name_asc'
end

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

Overriding Rails Default Routing Helpers

I'm writing an app where I need to override the default routing helpers for a model. So if I have a model named Model, with the corresponding helper model_path() which generates "/model/[id]". I'd like to override that helper to generate "/something/[model.name]". I know I can do this in a view helper, but is there a way to override it at the routing level?
You can define to_param on your model. It's return value is going to be used in generated URLs as the id.
class Thing
def to_param
name
end
end
The you can adapt your routes to scope your resource like so
scope "/something" do
resources :things
end
Alternatively, you could also use sub-resources is applicable.
Finally you need to adapt your controller as Thing.find(params[:id]) will not work obviously.
class ThingsController < ApplicationController
def show
#thing = Thing.where(:name => params[:id).first
end
end
You probably want to make sure that the name of your Thing is unique as you will observe strange things if it is not.
To save the hassle from implementing all of this yourself, you might also be interested in friendly_id which gives you this and some additional behavior (e.g. for using generated slugs)
You need the scope in routes.rb
scope "/something" do
resources :models
end

How to set defaults for parameters of url helper methods?

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

Resources