Rails routing more complex :constraints - ruby-on-rails

Currently if you wish to add a constraint there are many ways to do it but as I see currently you can only include one definitive method which is called. E.g.
Class Subdomain
# Possible other `def`s here, but it's self.matches? that gets called.
def self.matches?( request )
# Typical subdomain check here
request.subdomain.present? && request.subdomain != "www"
end
end
The problem with the above approach is it doesn't handle routes prefixed in www, that is admin and www.admin are indistuingishable. More logic can be added, but if this was required over a set of static subdomains like admin, support, and api you currently need to make SubdomainAdmin, SubdomainSupport etc....
This can be solved with regex as follows in routes.rb:
admin
:constraints => { :subdomain => /(www.)?admin/ }
api
:constraints => { :subdomain => /(www.)?api/ }
If requests were even more complex than this things get tricky. So is there a way to add individual methods inside a class used for constraints?
Essentially, how is the below achieved? Is it even possible? Whats the best method of white-listing subdomains to use?
E.g.
Class Subdomain
def self.admin_constraint( request )
# Some logic specifically for admin, possible calls to a shared method above.
# We could check splits `request.subdomain.split(".")[ 1 ].blank?` to see if things are prefixed with "www" etc....
end
def self.api_constraint( request )
# Some logic specifically for api, possibly calls to a shared method above.
# We could check splits `request.subdomain.split(".")[ 1 ].blank?` to see if things are prefixed with "www" etc....
end
def self.matches?( request )
# Catch for normal requests.
end
end
With which we can now call constraints specifically as follows:
:constraints => Subdomain.admin_constraints
And all generic constraints as follows:
:constraints => Subdomain
Is this possible in Rails 4.0.3?

The router will call the #matches?(request) method on whatever object you pass the route. In the case of
:constraints => Subdomain
you're giving the route the Subdomain Class object. However, you could also pass an instance, which you could configure via arguments. e.g.,
Class Subdomain
def initialize(pattern)
#pattern = pattern
end
def matches?(request)
request.subdomain.present? && #pattern ~= request.subdomain
end
end
# routes.rb
namespace :admin, constraints: Subdomain.new(/(www.)?admin/) do
# your admin routes here.
end
NOTE: I didn't verify that code works, I just wrote it off the top of my head, so consider it more of pseudo-code than implementation ready.
Also, you can see an example of this technique, with some more details, at: Use custom Routing Constraints to limit access to Rails Routes via Warden.

Related

Route Constraints Not Working As Expected

I hoped route constraints would allow me to have admin.example.com/widgets/ and example.com/admin/widgets/ be effectively the same, with the URL helper admin_widgets_path pointing to the correct one based on the current subdomain. However, the route helper seems to only point to one or the other regardless of constraints.
Am I doing something wrong? Is this a bug? Is there a better way to solve this problem?
A rails app with an example of the problem has been published here, but the relevant details are below.
My config/routes.rb
class Subdomains
# any domain that starts with 'admin.'
def self.admin?
-> (request) { request.host =~ /^admin\./ }
end
# any other domain
def self.primary?
-> (request) { !admin?.(request) }
end
end
Rails.application.routes.draw do
constraints(Subdomains.primary?) do
namespace :admin do
resources :widgets
end
end
constraints(Subdomains.admin?) do
scope module: "admin", as: :admin do
resources :widgets
end
end
root to: "admin/widgets#index"
end
My app/views/admin/widgets/index.html.erb
<%= link_to "Widgets", admin_widgets_url %>
In this configuration, admin_widgets_url always returns /admin/widgets/ which isn't a valid route on admin.example.com so clicking the link results in a routing error on that subdomain. If the admin subdomain constraint block is put first in the routes, then the URL helper always returns /widgets/, breaking the link on a non-admin domain.
The routes otherwise work, but not sure how to get the URL helpers to point to the correct one.
Your expectations are wrong - Rails reads the entire routes definition as part of the setup phase. This is also when the route helpers are created. The constraint is not actually evaluated until later when the request is matched.
Rails splits this into distinct phases to allow forking.
Instead you may need to override the route helpers.

Rails routes, constraints and variable scope

I have the following wildcard routes & constraints setup ...
get '*path' => 'profiles#show', constraints: SlugConstraint.new
get '*path' => 'blogs#show', constraints: SlugConstraint.new
and
class SlugConstraint
def initialize
#slugs = Slug.all.map(&:name)
end
def matches?(request)
request.url =~ /\/(.+)/
#slugs.include?($1)
end
end
... a variation based on the issue I described here:
Rails wildcard route with database lookup & multiple controllers
My issue now is that if the first call to SlugConstraint.new returns false (so that the 2nd routes.rb SlugConstraint.new now gets called) I don't want to have to redo the call to:
Slug.all.map(&:name)
How do I properly save (or scope) the #slugs data from the first constraint call that failed, so that I can access it if needed in the next constraint call?
Thanks.
Routing
You're not going to be able to use 2 routing patterns for the same path
When you send a request to Rails (or any other MVC application), Rails will take the path you've sent & consequently try to assign the right route (controller#action) for it.
This happens sequentially - IE Rails will look through your routes from top -> bottom until it finds one which corresponds. As you have two routes to match the same path, you're not going to be able to use the set up you have
--
App-Wide Slugs
What you're looking for is something called app-wide slugs - which essentially means you're able to manage a single slug path, and have a system in the back-end to accommodate it.
You're on the brink of being able to achieve this, and whilst I don't have any code to help, I do have an idea, which I found here:
#config/routes.rb
get '*path' => MyRouter.new, constraints: SlugConstraint.new
#lib/my_router.rb
class MyRouter
def call(env)
# Matched from routes, you can access all matched parameters
view_name= env['action_dispatch.request.path_parameters'][:view_name]
# Compute these the way you like, possibly using view_name
controller= 'post'
my_action= 'show'
controller_class= (controller + '_controller').camelize.constantize
controller_class.action(my_action.to_sym).call(env)
end
end
This will allow you to pick up the slugged paths, whilst routing to the correct controller. This is TOTALLY untested & just a stab in the dark - if you want to go over it with me, comment & we can have a look

Rails route scope default based on current_user

I have a route something like:
scope ":department", department: /admin|english|math/, defaults: { department: 'admin' }
Is it possible to make the default department for this route to be based on the current_user.department.name?
If this is not possible, what is another way of solving my problem. The problem is that I want all links to default to the current scope unless otherwise noted. I'm currently doing the following in LOTS of places:
# links to /math/students
link_to 'students', students_path(department: current_user.department.name.downcase)
If I understand correctly, what you want is to be able to write:
link_to 'students', students_path
and have the department option automatically set based on the current user.
Here's a solution that's like some of the others that have been offered: define helpers for each route that requires a department. However, we can do this programmatically.
Here we go:
app/helpers/url_helper.rb
module UrlHelper
Rails.application.routes.routes.named_routes.values.
select{ |route| route.parts.include?(:department) }.each do |route|
define_method :"department_#{route.name}_path" do |*args|
opts = args.extract_options!
if args.size > 0
keys = route.parts - [:department]
opts.merge!(Hash[*keys.zip(args).flatten])
end
opts.reverse_merge!(department: current_user.department.name.downcase)
Rails.application.routes.url_helpers.send(:"#{route.name}_path", opts)
end
end
end
You now have helper methods like department_students_path for every route that has a :department path segment. These will work just like students_path -- you can pass in opts, you can even set the :department explicitly and it will override the default. And they stay up to date with changes to your routes.rb without you having to maintain it.
You might even be able to name them the same as the original helpers, i.e.,
define_method :"#{route.name}_path"
without having to prefix them with department_--I didn't do that because I'd rather avoid naming collisions like that. I'm not sure how that would work (which method would win the method lookup when calling it from a view template), but you might look into it.
You can of course repeat this block for the _url helper methods, so you'll have those in addition to the _path ones.
To make the helpers available on controllers as well as views, just include UrlHelper in your ApplicationController.
So I think this satisfies your criteria:
You can call a helper method for paths scoped to :department which will default to the current_user's department, so that you don't have to explicitly specify this every time.
The helpers are generated via metaprogramming based on the actually defined named routes that have a :department segment, so you don't have to maintain them.
Like the built-in rails url_helpers, these guys can take positional args for other path segments, like, department_student_path(#student). However, one limitation is that if you want to override the department, you need to do so in the final opts hash (department_student_path(#student, department: 'math')). Then again, in that case you could always just do student_path('math', #student), so I don't think it's much of a limitation.
Route contraints can accept a proc, so you could solve this problem with the following "hack":
concern :student_routes do
# all your routes where you want this functionality
end
# repeat this for all departments
scope ":department", constraints: lambda{|req| req.session[:user_id] && User.find(req.session[:user_id]).department.name.downcase == "english"}, defaults: { department: 'english' } do
concerns :student_routes
end
Route concerns is a feature of rails 4. If you don't use rails 4, can you get the feature with this gem: https://github.com/rails/routing_concerns.
You can also just copy all the routes for students to all of the scopes.
You could use
link_to 'students', polymorphic_path([current_user.department.name.downcase, :students])
This will call
math_students_path
So to work I suppose you should add in your routes
scope ':department', as: 'department' do
...
as key generates helpers of the form: department_something_path
As that line is too long you could extract that to a helper
# students path taking into account department
def students_path!(other_params = {}, user = current_user)
polymorphic_path([user.department.name.downcase, :students], other_params)
Then you will use in your views
link_to 'students', students_path!
What about using nested routes? You could implement this with:
resources :departments do
resources :students
end
If you want to use a friendly url (like "math" instead of an id) can you add the following to the department model:
# in models/department.rb
def to_param
name.downcase
end
Remember name have to be unique for each department. Ryan B have made a railscast about this. You could also use a gem like friendly id.
You can now add the following to your views:
# links to /math/students
link_to 'students', department_students_path(current_user.department)
If you use this often, then create a helper:
# helpers/student_helpers.rb
def students_path_for_current_user
department_students_path(current_user.department)
end
# in view
link_to 'students', students_path_for_current_user
I ended up specifying it in the ApplicationController with:
def default_url_options(options={})
{department: current_user.department.name}
end
The path helpers will fill have the :department parameter filled in automatically then...

Ruby on Rails: Routing for subdomain and custom domain possible?

I'm looking to route users' pages from their subdomains and also their custom domains. For example, consider three domains:
app.com
user1.app.com
user1.com
A visitor should be able to see the user's page at both the subdomain from the app's domain (user1.app.com) as well as the user's custom domain (user1.com). That is, a visitor will visit the user page when visiting any subdomain of "app.com" or a root domain that is NOT "app.com".
How would I set up routes to do so?
Maybe something along the lines of this pseudo-code:
match "/", :to => "user_page#show", :constraints => { :subdomain => /.+/ OR :domain => NOT(app.com) }
What do you think?
use a constraint utility class or module.
module DomainConstraint
def self.matches? request
request.subdomain.present? || request.domain != 'app.com'
end
end
constraints DomainConstraint do
# routing here
end
if your constraint only applies to one route, you can do :
resources :foo, constraints: DomainConstraint
note : your utility class can also be replaced by a simple lambda (see "Dynamic request matching")

Routing Error with Post/Put requests (Passenger Headers)

I've run into a weird problem and after a bunch of research can't get any closer. I've got several forms that upload files via Carrierwave. When I upload the information, part of the route gets cut off (I think).
For example, I have a multi-part form submitting to:
https:/domain/programs/223/add_file as POST
but on submission I get the error
No route matches [POST] "/223/add_file"
even though what's in my address bar is the complete route. And if submit the complete route as a GET request it works fine. When I run rake routes the route shows up just fine.
Here is a subset of my route:
resources :programs do
match "add_file" => "programs#add_file"
If it matters, I'm running Rails 3.2.2 with Passenger on Apache. The problem only happens on this production server, never in development.
Any ideas? I'm stuck on this one as it effects multiple routes and I've tried defining a custom route just for that form with no luck.
Update:
When I remove multi-part => true or the file_field_tag from the form it fixes the problem. It's still an issue but seems to be less about routing than about the form with file uploads.
Create passenger_extension.rb in the lib folder with this code:
Passenger 3
module PhusionPassenger
module Utils
protected
NULL = "\0".freeze
def split_by_null_into_hash(data)
args = data.split(NULL, -1)
args.pop
headers_hash = Hash.new
args.each_slice(2).to_a.each do |pair|
headers_hash[pair.first] = pair.last unless headers_hash.keys.include? pair.first
end
return headers_hash
end
end
end
Passenger 5
module PhusionPassenger
module Utils
# Utility functions that can potentially be accelerated by native_support functions.
module NativeSupportUtils
extend self
NULL = "\0".freeze
class ProcessTimes < Struct.new(:utime, :stime)
end
def split_by_null_into_hash(data)
args = data.split(NULL, -1)
args.pop
headers_hash = Hash.new
args.each_slice(2).to_a.each do |pair|
headers_hash[pair.first] = pair.last unless headers_hash.keys.include? pair.first
end
return headers_hash
end
def process_times
times = Process.times
return ProcessTimes.new((times.utime * 1_000_000).to_i,
(times.stime * 1_000_000).to_i)
end
end
end # module Utils
end # module PhusionPassenger
And then in 'config/application.rb' do:
class Application < Rails::Application
...
config.autoload_paths += %W(#{config.root}/lib)
require 'passenger_extension'
end
And then restart a webserver.
NOTICE: I'm not sure that this doesn't break any other functionality so use it on your own risk and please let me know if you find any harm from this approach.
One issue here is you're not specifying whether the route is defined on the collection or a member. Which one of these is the correct route?
programs/:id/add_file
programs/add_file
You should construct your routes like this:
resources :programs do
post 'add_file', :on => :member
end
or
resources :programs do
member do
post 'add_file'
end
end
The above will take post requests at programs/:id/add_file and send them to ProgramsController.add_file with the params[:id] as the program id.
If you want this on the collection, you could do:
resources :programs do
post 'add_file', :on => :collection
end
or
resources :programs do
collection do
post 'add_file'
end
end
This would take post requests at programs/add_file and send them to ProgramsController.add_file, but no params[:id] would be set.
In general you should always specify whether routes are on the collection or member, and you should specify which verb a route should accept (ie use 'get' or 'post' etc. instead of 'match').
Try the above and see if that solves your problem, if not please let me know and I'll take another look.
I think you may need to add
:via => [:post]
to your route specification. It seems odd that it'd work on development and not on production, but as I understand rails routing, the matcher that you've added is only going to respond to get.
Try changing your match to
match "add_file" => "programs#add_file", :via => [:post]
Also, based on the answer just submitted by Andrew, you're probably better off using the member specifier to be explicit about the fact that the operation is happening on a particular Program with a particular id, and not the collection. It also should save some code in your add_file method which is probably working hard to get the id parameter from the url.

Resources