Rails 4 url_for with host constraint - ruby-on-rails

We created a multi tenancy application with multiple sites and each site is able to modify the routes from a backend.
in routes.rb we load the dynamic routes for all sites and put them into a host constraint like this
routes.rb
Frontend::Application.routes.draw do
DynamicRoutes.load
end
app/models/dynamic_routes.rb
class DynamicRoutes
# dynamically loads the routes from settings into the routes.rb file
# and adds a host constraint to just match with the current sites host
# http://codeconnoisseur.org/ramblings/creating-dynamic-routes-at-runtime-in-rails-4
def self.load
if Site.table_exists?
Frontend::Application.routes.draw do
Site.includes(:setting).each do |site|
site.routes.each do |route|
# write the route with the host constraint
constraints(:host => site.hostname) do
case route[0]
when :shop_show
match "#{route[1]}", to: 'shops#show', via: [:get], as: "shop_show_#{site.id}"
end
end
end
end
end
end
end
# allows to reload the routing
# e.g. when changes in route settings where made
#
def self.reload
Rails.application.reload_routes!
end
end
So we create all routes for each site and match them with a host constraint. This works fine unless we use the url_for helper
#site = Site.find_by(hostname: request.host)
url_for controller: 'shop', action: 'show', host: #site.hostname
url_for returns the first matching url, doesnt matter from which host it should belong to. so the host constraint is not used, even if I put a host: param
Do you have any idea, of how its possible to use url_for with host constraints?

I have had the same task in my application. url_for ignores host param. But we could create additional path helpers in our ApplicationController in the following way:
ApplicationController.rb
%w( shop_show ).each do |helper|
helper_name = "#{helper}_path".to_sym
helper_method helper_name
define_method(helper_name) { |*args| send "#{helper}_#{site.id}_path", *args }
end
After that you are able to use universal path shop_show_path in your views. Of course, you should dynamically assign site variable depending on your host/domain.

Related

Customising rails Routes for user - Rails 4

could one advise me how to get a url like this in rails
http://www.example.com/users/5/ian
i tried the below but unsure:
route file:
devise_for :users
resources :users do
resources :socials
end
get '/users/:id/:firstname', controller: 'users', action: 'show'
users_controller.rb
def show
#user = User.find(params[:id], params[:firstname])
end
If you are trying to achieve 'friendly urls' then I suggest using this:
You don't have to create a special route:
get '/users/:id', controller: 'users', action: 'show'
Instead you have your model overwrite the to_param method:
class User
...
def to_param
"#{id}-#{firstname.try(:parameterize)}"
end
...
end
The url helper calls to_param to build the urls. If you overwrite it this way, you will receive a url like this:
http://localhost:3000/users/1-artloe
The rails find method calls .to_i on the params[:id] which, thankfully, interprets strings as number until it arrives at a character that can't become a number.
Examples:
'123abcde'.to_i # 123
'123-asdf'.to_i # 123
'asdf-123'.to_i # 0
So except for overwriting to_param, you don't have to do anything.
Try replacing this
def show
#user = User.find_by_id_and_firstname(params[:id], params[:firstname])
end
If what you are trying accomplish is "friendly urls" you would do it by:
# GET /users/1
# GET /users/joe
def show
#user = User.find_by!('id = :x OR firstname = :x', x: params[:id])
end
However you must ensure that property you are using in URLs is URL safe and unique. Usually a separate username or slug field is used.
Nothing special is needed in terms of routes.
These gems provide "friendly urls":
stringex
friendly_id

Get constraint based url using url_for based on mounted engine

Is there any way that i can make url_for to return the url based on the request.host during action dispatch routing ?
mount Collaborate::Engine => '/apps/collaborate', :constraints => {:host => 'example.com' }
mount Collaborate::Engine => '/apps/worktogether'
Example:
When the user is on example.com host
collaborate_path => /apps/collaborate
When the user is on any other host
collaborate_path => /apps/worktogether
After a lot of research, i realize that RouteSet class has named_routes which does not consider the constraints to return the url.
I've tried overriding #set in action_dispatch/routing/route_set.rb to pickup from rails application but dint work as expected
#search_set = Rails.application.routes.set.routes.select{|x| x.defaults[:host] == options[:host] }[0]
#set = #search_set unless #search_set.blank?
Remove .com in your example
mount Collaborate::Engine => '/apps/collaborate', :constraints => {:host => 'examplesite' }
mount Collaborate::Engine => '/apps/worktogether'
Should just work
If you need a more advanced constraint, make your own constraint:
class CustomConstraint
def initialize
# Things you need for initialization
end
def matches?(request)
# Do your thing here with the request object
# http://guides.rubyonrails.org/action_controller_overview.html#the-request-object
request.host == "example"
end
end
Rails.application.routes.draw do
get 'foo', to: 'bar#baz',
constraints: CustomConstraint.new
end
You can also specify constraints as a lambda:
Rails.application.routes.draw do
get 'foo', to: 'foo#bar',
constraints: lambda { |request| request.remote_ip == '127.0.0.1' }
end
source: http://guides.rubyonrails.org/routing.html#advanced-constraints
As for as my concern if you handle it at middleware level then it would be good. This is what my assumption.
Add this line in config/application.rb
config.middleware.insert_before ActionDispatch::ParamsParser, "SelectiveStack"
Add a middleware in app directory with middleware directory as a Convention
app/middleware/selective_stack.rb
class SelectiveStack
def initialize(app)
#app = app
end
def call(env)
debugger
if env["SERVER_NAME"] == "example.com"
"/apps/collaborate"
else
"/apps/worktogether"
end
end
end
Hope this will solve your issue.!!!
Alright, here's a shot in the dark; maybe you've tried it already or maybe I'm really missing something. On the surface, it really looks like you're just trying to override a path helper method for apps. So why not set up an override in the application_helper.rb? Something like:
module ApplicationHelper
def collaborate_path
if request.domain == "example.com"
"/apps/collaborate"
else
"/apps/worktogether"
end
end
end

Rails routes: different domains to different places

I've got a Rails app up running on a server. It's a big project so there are lots of routes involved, and two domains point to the root at the moment. I'd like to somehow design my routes.rb to interpret one domain to take it to a certain part of the app as if it was the root, and use the other for everywhere else.
Something like this (very pseudocode, hope you get the idea):
whole_app.com
whole_app.com/documents
whole_app.com/share
whole_app.com/users
partial_app.com, :points_to => 'whole_app.com/share'
Can Rails handle this? Thank-you!
You can achieve this by overriding default url_options method in application controller. This will override host url for every request.
class ApplicationController < ActionController::Base
....
def default_url_options
if some_condition
{:host => "partial_app.com"}
else
{:host => "whole_app.com"}
end
end
....
end
And for pointing a route to some specific url, you may use:
match "/my_url" => redirect("http://google.com/"), :as => :my_url_path
The better way is to do settings on server to redirect some url to a specific location.
is it going to /share based on some kind of criteria? if so you can do this:
routes.rb
root :to => 'pages#home'
pages_controller.rb
def home
if (some condition is met)
redirect_to this_path
else
render :layout => 'that'
end
end

How can I remove subdomain when the user accessed to www.xxxxxxxxxx.com?

Now in my app, the user can access to both www.xxxxxxxxxx.com and xxxxxxxxxx.com
but I don't want to use www.xxxxxxxxxx.com
How can I make the user redirect to xxxxxxxxxx.com?
My routing is like this
routes.rb
constraints(:subdomain => /^(|www)$/) do
root :to => "top#index"
end
I agree, doing this in the web server is better, however if you are unable to edit your virtual host configuration, you can create a filter like the following in the ApplicationController:
def strip_www
if request.env["HTTP_HOST"] == "www.url.com"
redirect_to "http://url.com#{request.request_uri}"
end
end

Rails Routes based on condition

I have three roles: Instuctor, Student, Admin and each have controllers with a "home" view.
so this works fine,
get "instructor/home", :to => "instructor#home"
get "student/home", :to => "student#home"
get "admin/home", :to => "admin#home"
I want to write a vanity url like below which will route based on the role of the user_id to the correct home page.
get "/:user_id/home", :to => "instructor#home" or "student#home" or "admin#home"
How do I accomplish this?
I'm providing an alternate approach as this SO question comes up near the top when searching for role based routing in Rails.
I recently needed to implement something similar but wanted to avoid having a large number of conditionals in the controller - this was compounded by the fact that each of my user roles required completely different data to be loaded and presented. I opted to move the deciding logic to the routing layer by using a Routing Constraint.
# app/constraints/role_route_constraint.rb
class RoleRouteConstraint
def initialize(&block)
#block = block || lambda { |user| true }
end
def matches?(request)
user = current_user(request)
user.present? && #block.call(user)
end
def current_user(request)
User.find_by_id(request.session[:user_id])
end
end
The most important part of the above code is the matches? method which will determine whether or not the route will match. The method is passed the request object which contains various information about the request being made. In my case, I'm looking up the :user_id stored in the session cookie and using that to find the user making the request.
You can then use this constraint when defining your routes.
# config/routes.rb
Rails.application.routes.draw do
get 'home', to: 'administrators#home', constraints: RoleRouteConstraint.new { |user| user.admin? }
get 'home', to: 'instructors#home', constraints: RoleRouteConstraint.new { |user| user.instructor? }
get 'home', to: 'students#home', constraints: RoleRouteConstraint.new { |user| user.student? }
end
With the above in place, an administrator making a request to /home would be routed the home action of the AdministratorsController, an instructor making a request to /home would be routed to the home action of the InstructorsController, and a student making a request to /home would be routed to the home action of the StudentsController.
More Information
If you're looking for more information, I recently wrote about this approach on my blog.
You can't do this with routes because the routing system does not have the information required to make this decision. All Rails knows at this point of the request is what the parameters are and does not have access to anything in the database.
What you need is a controller method that can load whatever data is required, presumably the user record, and redirects accordingly using redirect_to.
This is a fairly standard thing to do.
Update:
To perform all of this within a single controller action you will need to split up your logic according to role. An example is:
class HomeController < ApplicationController
def home
case
when #user.student?
student_home
when #user.admin?
admin_home
when #user.instructor
instructor_home
else
# Unknown user type? Render error or use a default.
end
end
protected
def instructor_home
# ...
render(:template => 'instructor_home')
end
def student_home
# ...
render(:template => 'student_home')
end
def admin_home
# ...
render(:template => 'admin_home')
end
end

Resources