When I visit http://my-application.com/posts/1 in my browser, Rails knows I'm looking for the Post with id = 1. How can I get my application to do this internally? I.e., I'd like a function (call it associate_with_resource) that takes a string containing a URL as its input and outputs the associated resource. For example:
>> associate_with_resource('http://my-application.com/posts/1')
=> #<Post id: 1, ... >
(I'd like to be able to use associate_with_resource throughout my application though -- not only in the console)
I think I'm looking for the ActionController::Routing::Routes.recognize_path method
You are right about ActionController::Routing::Routes.recognize_path and I would do it like this:
create a file lib/associate_with_resource.rb
module AssociateWithResource
def associate_with_resource(path)
url_hash = ActionController::Routing::Routes.recognize_path path
url_hash[:controller].classify.constantize.find(url_hash[:id])
end
end
class ActionController::Base
include AssociateWithResource
helper_method :associate_with_resource
end
class ActiveRecord::Base
include AssociateWithResource
end
Now you can call the associate_with_resource(path) from almost everywhere to get the resource belonging to a given path
When I visit http://my-application.com/posts/1 in my browser, Rails knows I'm looking for the Post with id = 1.
This is not correct.
In Rails 3, when you put this into routes.rb:
resources :posts
Then Rails will know that you have a controller named PostsController in the file app/controllers/posts_controller.rb. Rails will also know that in your PostsController class, you have seven methods that are intended to be action methods: index, new, create, show, edit, update, delete.
What you do in these action methods is entirely up to you. You may wish to retrieve and display a Post object, or not.
Related
Is it possible for the route below to dynamically select different controllers or at least for a single controller to dynamically call another controller?
get '*path' => 'routing#show
For example:
/name-of-a-person => persons#show
/name-of-a-place => places#show
I recall reading something about Rails 5 that would enable this but I can't find it again to save my life. It's possible I imagined it.
Another options is to have a RoutingController that depending on which path is received will call different controllers.
The use case is I have URLs in the database with a type, and the controller depends on what type is the URL. I'm thinking something like this:
get '*path' do |params|
url = Url.find_by!(path: params[:path])
case url.type
when 'person'
'persons#show'
when 'place'
'places#show'
end
end
I post my second best solution so far; still waiting to see if anyone knows how to do this efficiently within the routes.
class RoutingController < ApplicationController
def show
url = Url.find_by!(path: params[:path])
url.controller_class.dispatch('show', request, response)
end
end
Hat tip to André for the idea.
You could define one controller and inside its action make something like this:
def generic_show
url = Url.find_by!(path: params[:path])
case url.type
when 'person'
controller = PersonController.new
controller.request = request
controller.response = response
controller.show
when 'place'
...
end
end
However, I would recommend you to move the code you want to reuse to other classes and use them in both controllers. It should be easier to understand and maintain.
I think you may be able to do it using advanced routing constraints.
From: http://guides.rubyonrails.org/routing.html#advanced-constraints
If you have a more advanced constraint, you can provide an object that responds to matches? that Rails should use. Let's say you wanted to route all users on a blacklist to the BlacklistController. You could do:
class BlacklistConstraint
def initialize
#ips = Blacklist.retrieve_ips
end
def matches?(request)
#ips.include?(request.remote_ip)
end
end
Rails.application.routes.draw do
get '*path', to: 'blacklist#index',
constraints: BlacklistConstraint.new
end
I don't think the Rails guide example is particularly good, because this problem could essentially be solved in your application controllers before_action.
In this example, the constraint is used for IP filtering, but you could also implement matches? to check if it's a person. I would imagine something like
def matches?(request)
Person.where(slug: request.params[:path]).any?
end
And as such, the Rails router can decide whether or not to dispatch the request to the persons#show action.
I'm real beginner in Rails.
I created app/services/xclass.rb class with some_method inside.
I need to execute some_method using url.
For example, I want run this method when I execute in my browser url - http://application.com/notifications/send
I think it could be done through controller (notifications_controller) but how to do it?
I created only controller, with no model, just for launching some_method.
first, create a route:
get "notifications/send" => "notifications#some_action", :as => "send_notification"
Then create a controller action in your controller (ie. NotificationsController):
def some_action
Xclass.some_method # run the method you want
redirect_to root_path # redirect or whatever you want here
end
Now you can either visit the path http://your_app.com/notifications/send, or link to is using 'send_notifications_path' url helper in rails.
That should do it
Since you're a beginner, let me give you some ideas
MVC
Firstly, you need to appreciate that Rails is an MVC (model view controller) framework:
In short, this means that every time you send a "request" to Rails, it will be "routed" to the specific controller action which corresponds with that route.
This means that when you ask about how to fire a "class method", you're going to have to work within the confines of the MVC programming pattern. Here's how:
#config/routes.rb
resources :notifications do
get :send, on: :collection #=> domain.com/notifications/send
end
#app/controllers/notifications_controller.rb
class NotificationsController < ApplicationController
def send
#call your class method here
YourModel.class_method
end
end
#app/lib/your_model.rb
class YourModel
def self.class_method
#do something here
end
end
--
Rails
This is further supported by the fact that Rails is just a framework - in fact it's a gem (a great one) which runs on top of Ruby.
This means that even though some of the ways in which Rails works might seem somewhat alien to begin with, you have to remember that it basically just captures "requests" fed to it by a web sever, processing them with connectivity to the database etc.
The issue here is that as you're sending the request over HTTP, you have to work within the constraints of this protocol (specifically that it's stateless), and with Rails. As mentioned, Rails is MVC-based, which means that every request will be routed to your controller, which is why you have to create the corresponding route & controller action to handle it
If you use the code above (tweaked to your app), it should work for you
I have a controller, clients_controller, with corresponding index, show, edit, delete, new & form views. Is there a way to create a new view like clients/prospects.html.erb that acts the same way as clients/index.html.erb, except is routed at clients/prospects/?
I've tried this:
match '/clients/prospects' => 'clients#prospects'
And some other things in routes.rb, but of course get the error "Couldn't find Client with id=prospects".
The goal here is basically to have a prospects view and a clients view, and by simply switching the hidden field to a 1, it (in the user's mind) turns a prospect into a client (it's a CRM-like app).
There's a couple of things you need to do. First you need to put the your custom route before any generic route. Otherwise Rails assumes the word "prospects" is an id for the show action. Example:
get '/clients/prospects' => 'clients#prospects' # or match for older Rails versions
resources :clients
Also you need to copy / paste the index method in your ClientsController and name it prospects. Example:
class ClientsController < ApplicationController
def index
#clients = Client.where(prospect: false)
end
def prospects
#prospects = Client.where(prospect: true)
end
end
Lastly, you need to copy the index.html.erb view and name the copy prospects.html.erb. In the example above you would have to work with the #prospects instance variable.
Create a new action in clients controller named prospects. And then define a collection route in routes.rb for it as either resource full way. Or u directly use match as you were doing.
What you're doing is not wrong (although I'd change match to get, otherwise POST and DELETE requests to that url will also render your prospects view). Presumably you have
resources :clients
in your routes file? If so, what you have will probably work if you just move the line you quoted above the resources declaration -- the problem is that /clients/prospects matches the show route for the clients resource, so if it's defined first then that's the route that gets matched.
However, there's a more idiomatic way to define this route
resources :clients do
collection do
get :prospects
end
end
See Rails Routing documentation for more
Also see migu's answer for what else needs to be done once the url is being routed correctly (though there are other things you can do -- if you the two views are similar enough, you can reuse the view template, for example).
I need to have one path accessible through multiple names. In my routes.rb I did
get '/route' => 'controller#edit', :as => 'name_a'
get '/route' => 'controller#edit', :as => 'name_b'
This works nicely but loads the routes table for nothing. From my understanding of the documentation, :as defines a helper method when called.
So I went to my ApplicationController and added
alias_method :name_b, :name_a
and I removed the second line from routes.rb
but that fails with Uncaught exception: undefined method name_a for class ApplicationController
is there any proper way of having two names for a single path?
=================EDIT====================
Elaboration:
I use Devise gem to manage session, registration, locking, etc. of 2 kinds of users, let's call them Admin and Guest. The gem is very well put but it asks for definitive route names to behave properly.
In my case, as far as devise is concerned, only the registration process is different so I'm trying to build a structure which looks as follow:
app
controllers
users
admin
registration_controller.rb
guest
registration_controller.rb
session_controller.rb
password_controller.rb
registration_controller.rb
the Admin and Guest controllers inherit from the above registration_controller which inherit's from Devise.
Now, to work properly, Devise needs for instance the names guest_user_password and admin_user_password to create or delete password retrievals. In my case, both are under the same path so I want both names to redirect to the same 'users/password' controller.
More important, and that's why I really wanted the alaising. Is that my views should not care whether it is dealing with Admin and Guest routes when redirecting to password retrieval controller. Both are users so I want to use user_password for both.
Hence my question. :)
Also note that as I wrote it, things works. I'm just trying to get the 'most elegant way' of writing it.
How about putting the alias in your ApplicationController?
class ApplicationController < ActionController::Base
alias_method :route_new, :route_old
helper_method :route_new
Remember that it's new name first, then old name.
The helper_method call is in order to use these in your views and not just controllers.
If you like, you can then place this in an included module called something like RouteAliases
You can add something like this to your routes.rb:
Rails.application.routes.draw do
...
Rails.application.routes.named_routes.tap do |named_routes|
named_routes['new_name'] = named_routes['real_name']
end
end
This will create new_name_path and new_name_url helpers. I have tested this with Rails 5.0.6.
I am using ruby on rails to make a simple social networking site that includes different message boards for each committee of a student group. I want the url structure for each board to look like https://<base_url>/boards/<committee_name> and this will bring the user to the message board for that committee.
My routes.rb file looks like:
resources :committees, only: [:index]
match '/boards/:name', to: 'committees#index(name)'
My index function of committees_controller.rb file looks like:
def index(name)
#posts = Committee.where(name: name)
end
And then I'll use the #posts variable on the page to display all of the posts, but right now when I navigate to https://<base_url>/boards/<committee_name> I get an Unknown Action error, and it says The action 'index(name)' could not be found for CommitteesController.
Could someone guide me through what I have done wrong?
Once I get this working, how would I make a view that reflects this url structure?
Set up your routes like this:
resources :committees, only: [:index]
match '/boards/:name', to: 'committees#show'
and the controller like this:
def index
#committees = Committee.all
end
def show
#committee = Committee.find_by_name!(params[:name])
end
You can't really pass arguments to controller actions the way you were trying to with index(name). Instead, you use the params hash that Rails provides you. The :name part of the route declaration tells Rails to put whatever matches there into params[:name].
You also should be using separate actions for the listing of committees and displaying single committees. Going by Rails conventions, these should be the index and show actions, respectively.
When routing, you only specify the method name, not the arguments:
match '/boards/:name', to: 'committees#show'
Generally you will declare something with resources or match but not both. To stay REST-ful, this should be the show method. Index is a collection method, usually not taking any sort of record identifier.
Arguments always come in via the params structure:
def show
#posts = Committee.where(name: params[:name])
end
Controller methods that are exposed via routes do not take arguments. You may construct private methods that do take arguments for other purposes.