you786
you786

Reputation: 3540

How to conditionally include route parameter in Rails?

I'm trying to allow for one resource, Site, to conditionally have a parent resource, Organization.

Here's what I currently have:

resources :site do
  resources :posts do
    resources :comments
  end
end

this results in paths like

/sites/1
/sites/1/edit
/sites/1/posts
/sites/1/posts/123/comments
# etc. etc.

I want the ability to have paths like

/parent_id/sites/1
/parent_id/sites/1/edit
/parent_id/sites/1/posts
/parent_id/sites/1/posts/123/comments

but only if the Site belongs to an Organization.

I also don't want to have to change every single path helper already in use across my site (there are literally hundreds of places).

Is this possible?

Here's what I've tried:

scope "(:organization_id)/", defaults: { organization_id: nil } do
  resources :site
end

# ...

# in application_controller.rb
def default_url_options(options = {})
  options.merge(organization_id: @site.organization_id) if @site&.organization_id
end

but that didn't work. organization_id wasn't getting set.

# in my views
link_to "My Site", site_path(site)
# results in /sites/1 instead of /321/sites/1

I also tried setting the organization_id in a route constraint, and that didn't work as well.

Upvotes: 3

Views: 795

Answers (3)

you786
you786

Reputation: 3540

I ended up writing a monkey patch overriding the dynamic method generated for all of my relevant paths. I used a custom route option, infer_organization_from_site that I look for when dynamically generating routes. If the option is set, I add site.organization as the first argument to the helper call.

# in config/routes.rb

scope "(:organization_id)", infer_organization_from_site: true do
  resources :sites do
    resources :posts
    # etc.
  end
end 

# in an initializer in config/initializers/

module ActionDispatch
  module Routing
    # :stopdoc:
    class RouteSet
      class NamedRouteCollection

        private

        # Overridden actionpack-4.2.11/lib/action_dispatch/routing/route_set.rb
        # Before this patch, we couldn't add the organization in the URL when
        # we used eg. site_path(site) without changing it to site_path(organization, site).
        # This patch allows us to keep site_path(site), and along with the proper
        # optional parameter in the routes, allows us to not include the organization
        # for sites that don't have one.
        def define_url_helper(mod, route, name, opts, route_key, url_strategy)
          helper = UrlHelper.create(route, opts, route_key, url_strategy)
          mod.module_eval do
            define_method(name) do |*args|
              options = nil
              options = args.pop if args.last.is_a? Hash
              if opts[:infer_organization_from_site]
                args.prepend args.first.try(:organization)
              end
              helper.call self, args, options
            end
          end
        end
      end
    end
  end
end

Upvotes: 1

gwcodes
gwcodes

Reputation: 5690

You're very close with your current approach. The problem appears to be that defaults on the routing scope takes precedence over default_url_options, so you'll end up with a nil organization ID every time.

Try instead, just:

# in routes.rb
scope "(:organization_id)" do
  resources :site
  ...
end

# in application_controller.rb
def default_url_options(options = {})
  # either nil, or a valid organization ID
  options.merge(organization_id: @site&.organization_id)
end

Upvotes: 0

Hackman
Hackman

Reputation: 1729

Add another block to your routes with the companies resources wrapped around it:

resources :companies do
  resources :site do
    resources :posts do
      resources :comments
    end
  end
end

resources :site do
  resources :posts do
    resources :comments
  end
end

Now you can create a helper for your links like this:

# sites_helper.rb
module SitesHelper
  def link_to_site(text, site)
    if site.company
      link_to text, company_site_path(site.company, site)
    else
      link_to text, site_path(site)      
    end
  end
end

Then use it in your view like this:

<%= link_to_site("Text of the link", variable_name) %>

Notice the variable_name in the arguments. It can be site or @site, depending on your code. Inside a loop it will probably be site but on a show page I guess it will be @site.

Upvotes: 1

Related Questions