Rails route scope default based on current_user - ruby-on-rails

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...

Related

Rails routing more complex :constraints

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.

Rails 4.1.2 - to_param escapes slashes (and breaks app)

I use in my app to_param to create custom URL (this custom path contains slashes):
class Machine < ActiveRecord::Base
def to_param
MachinePrettyPath.show_path(self, cut_model_text: true)
end
end
The thing is, that since Rails 4.1.2 behaviour changed and Rails doesn't allow to use slashes in the URL (when use custom URL), so it escapes slashes.
I had such routes:
Rails.application.routes.draw do
scope "(:locale)", locale: /#{I18n.available_locales.join("|")}/ do
resources :machines, except: :destroy do
collection do
get :search
get 'search/:ad_type(/:machine_type(/:machine_subtype(/:brand)))', action: 'search', as: :pretty_search
get ':subcategory/:brand(/:model)/:id', action: 'show', as: :pretty
patch ':subcategory/:brand(/:model)/:id', action: 'update' # To be able to update machines with new rich paths.
end
end
end
end
I tried by recommendation in the thread to use glob param just for show method to make sure it works:
resources :machines, except: :destroy do
#...
end
scope format: false do
get '/machines/*id', to: "machines#show"
end
But it absolutely doesn't work. I still have such broken links:
http://localhost:3000/machines/tractor%2Fminitractor%2Fmodel1%2F405
Of course, if I replace escaped slashes on myself:
http://localhost:3000/machines/tractor/minitractor/model1/405
And try to visit path, then page'll be opened.
Any ideas how can I fix that?
I've been having the same problem when using the auto-generated url helpers. I used a debugger to trace the new behavior to its source (somewhere around ActionDispatch::Journey::Visitors::Formatter), but didn't find any promising solutions. It looks like the parameterized model is now strictly treated as a single slash-delimited segment of the path and escaped accordingly, with no options to tell the formatter otherwise.
As far as I can tell, the only way to get the url helper to produce the old result is to use your original routes file and pass each segment separately, something like:
pretty_machine_path(machine.subcategory, machine.brand, machine.model, machine.id)
This is ugly as hell and obviously not something you'll want to do over and over. You could add a method to MachinePrettyPath to generate the segments as an array and explode the result for the helper (say, pretty_machine_path(*MachinePrettyPath.show_path_segments(machine))) but that's still pretty verbose.
Between the above headaches and the "You're Doing it Wrong" attitude from the devs in that Rails ticket you linked to, the simplest option for me was to bite the bullet and write a custom URL helper instead of using to_param. I've yet to find a good example of the "right" way to do that, but something like this bare-bones example should serve the purpose:
#app/helpers/urls_helper.rb
module UrlsHelper
def machine_path(machine, options = {})
pretty_machine_path(*MachinePrettyPath.show_path_segments(machine), options)
end
end
#app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
helper :urls #for the views
include UrlsHelper #for controllers
#...
end
If you're sure the returned url is safe you should add .html_safe to the returned string:
MachinePrettyPath.show_path(self, cut_model_text: true).html_safe
(didn't see anywhere else where it can be escaped but check all the flow in your app, maybe manually testing method by by method)
How about defining the url yourself?
def to_param
"#{ subcategory.title.parameterize }/#{ brand.name.parameterize }/#{ model.name.parameterize) }/#{ id }"
end
And then in your routes something like this:
get 'machines/*id', to: "machines#show"
You also have to split your params[:id] when you do a find on your model.
Machine.find( params[:id].split("/").last )

Params and selecting the right one

I've got this helper method in my application controller:
def current_team
#current_team ||= Team.find(params[:team_id])
end
Problem is, it works for urls of the format:
/teams/20/members/11
but it doesn't work for:
/teams/20
In order to get it to work for those, I have to change :team_id to be :id.
How can I tidy it up so it 'just works'?
Thanks!
Set instance variables (#current_team) in controllers, never in helpers. It's not what helpers are for.
If you follow this advice, you will naturally use params[:id] in TeamsController, but params[:team_id] in MembersController.
(Some people even go on to say that you shouldn't use helpers at all. For facilitating presentation (custom links, buttons, tables, etc), they propose to use Presenter pattern. But you don't have to listen to them. :))
It is not the best thing to do, but to accomplish that you can do the following:
def current_team
#current_team ||= Team.find(params[:team_id].presence || params[:id])
end
Documentation about the Object.presence method:
http://api.rubyonrails.org/classes/Object.html#method-i-presence
#SergioTulentsev is right, you shall not set instance variables in helpers, only in controllers.
I'm assuming you have other resources besides just Team. Rails is going to use the :id param for all of your resources. You will need to look into customizing the routes for your teams#show action. Easier in Rails 4 than in Rails 3.
Have a look at this post for the gory details: Change the name of the :id parameter in Routing resources for Rails
I wouldn't do params[:team_id] || params[:id], because of course in some controller contexts you'd get an id parameter that represents the id for something other than a Team. Assuming that the /teams/:id route is handled by the TeamsController, then you could do the following (to keep your method in ApplicationController and avoid repeating yourself in different controllers):
def current_team
id = controller_name == "teams" ? params[:id] : params[:team_id]
#current_team ||= Team.find(id)
end
Alternatively, you could change your routes so that the url to show a Team is /teams/:team_id and leave your helper as-is, but that would go against the grain of Rails routing conventions.

How to override routes path helper in rails?

My route is defined like this
match '/user/:id' => 'user#show', :as => :user
If for some reason a nil ID is passed I want the route helper to return only '#' and if ID is not nil i want it to return normal path like '/user/123'.
or is there any better way to do it.
this route helper has been used in lot of places in my code so I dont want to change that. Instead I am looking for one place which will effect all instances of user_path.
Thanks
I did this in app/helpers/url_helper.rb
module UrlHelper
def resource_path(resource, parameters = {})
# my implementation
"/#{resource.category}/#{resource.name}#{ "?#{parameters.to_query}" if parameters.present? }"
end
end
I know there are ways to define such a nested route but I have a codebase using that route in several parts, as the question states.
I tried to alias old method, but was not recognised:
alias_method :old_resource_path, :resource_path
module CustomUrlHelper
def user_path(user, options = {})
if user.nil?
"#"
else
super
end
end
end
# Works at Rails 4.2.6, for older versions try http://stackoverflow.com/a/31957323/474597
Rails.application.routes.named_routes.url_helpers_module.send(:include, CustomUrlHelper)

How to implement "short" nested vanity urls in rails?

I understand how to create a vanity URL in Rails in order to translate
http://mysite.com/forum/1 into http://mysite.com/some-forum-name
But I'd like to take it a step further and get the following working (if it is possible at all):
Instead of:
http://mysite.com/forum/1/board/99/thread/321
I'd like in the first step to get to something like this: http://mysite.com/1/99/321
and ultimately have it like http://mysite.com/some-forum-name/some-board-name/this-is-the-thread-subject.
Is this possible?
To have this work "nicely" with the Rails URL helpers you have to override to_param in your model:
def to_param
permalink
end
Where permalink is generated by perhaps a before_save
before_save :set_permalink
def set_permalink
self.permalink = title.parameterize
end
The reason you create a permalink is because, eventually, maybe, potentially, you'll have a title that is not URL friendly. That is where parameterize comes in.
Now, as for finding those posts based on what permalink is you can either go the easy route or the hard route.
Easy route
Define to_param slightly differently:
def to_param
id.to_s + permalink
end
Continue using Forum.find(params[:id]) where params[:id] would be something such as 1-my-awesome-forum. Why does this still work? Well, Rails will call to_i on the argument passed to find, and calling to_i on that string will return simply 1.
Hard route
Leave to_param the same. Resort to using find_by_permalink in your controllers, using params[:id] which is passed in form the routes:
Model.find_by_permalink(params[:id])
Now for the fun part
Now you want to take the resource out of the URL. Well, it's a Sisyphean approach. Sure you could stop using the routing helpers Ruby on Rails provides such as map.resources and define them using map.connect but is it really worth that much gain? What "special super powers" does it grant you? None, I'm afraid.
But still if you wanted to do that, here's a great place to start from:
get ':forum_id/:board_id/:topic_id', :to => "topics#show", :as => "forum_board_topic"
Take a look at the Rails Routing from the Outside In guide.
maybe try something like
map.my_thread ':forum_id/:board_od/:thread_id.:format', :controller => 'threads', :action => 'show'
And then in your controller have
#forum = Forum.find(params[:forum_id])
#board = #forum.find(params[:board_id])
#thread = #board.find(params[:thread_id])
Notice that you can have that model_id be anything (the name in this case)
In your view, you can use
<%= link_to my_thread_path(#forum, #board, #thread) %>
I hope this helps

Resources