Dynamic Routes Rails 4, taken from db - ruby-on-rails

Frustrating, I can't find an eligible solution for my problem.
In my Rails 4 app, I want to give my users the possibility to add their own custom post types to their sites. Like:
www.example.com/houses/address-1
www.example2.com/sports/baseball
Both would work, but only for the linked sites. Sports and houses would be the (RESTful) post types, taken from the db, added by users.
I have been struggling to find a elegant solution to accomplish this. I found http://codeconnoisseur.org/ramblings/creating-dynamic-routes-at-runtime-in-rails-4 but that feels kinda hacky and I'm not sure if reloading the routes works in production, I'm getting signals that it won't.
I'd say I have to use routes constraints http://guides.rubyonrails.org/routing.html#advanced-constraints but I don't have a clue how to approach this.
To be clear, I have no problem with the site setting stuff, the multi tenancy part of my app is fully functional (set in Middleware, so the current site is callable in the routes.rb file). My issue is with the (relative) routes, and how they could be dynamically set with db records.
Any pointers much appreciated.

I think route constraints don't work for you because your domain is a variable here. Instead, you should be examining the request object.
In your ApplicationController, you could define a method that would be called before any action, like so:
class ApplicationController < ActionController::Base
before_action :identify_site
def identify_site
#site = Site.where(:domain => request.host).first
end
end
As you scale, you could use Redis for your domains so you're not making an expensive SQL call on each request.
Then you can just add the #site as a parameter to whatever call you're making. I'm assuming you're doing some sort of "Post" thing, so I'll write some boilerplate code:
class PostController < ApplicationController
def show
#post = Post.where(:site => #site, :type => params[:type], :id => params[:id])
end
end
Just write your routes like any other regular resource.

Related

Rails 5 routing: Dynamic controller

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.

Rails CanCan: reference to resource_instance from application_controller

I have a bunch of controllers that extend the ApplicationController and each one loads a different cancan resource, e.g. UsersController -> #user, PostsController -> #post. I was wondering, is it possible to reference the resource from the ApplicationController without knowing the instance variable name? Something like resource_instance.
Ok, I got my final answer and it's No. I explored cancancan, cancan's well maintained spiritual child and the code is there, but is not meant to be used by the end user.
You could get what you want, it isn't pretty:
self.class.cancan_resource_class.new(self).send(:resource_instance)
That said, please don't. cancan_resource_class isn't documented to to be used by end users and :resource_instance is protected, hence the send. The developers could choose to change this and break your application.
If all have a company, they belong too, I recommend you nest the routes, so all routes have a :company_id you could use to get the #company object you want: companies/:company_id/users, etc.

How to run class method via URL in Rails 4

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

StackOverflow Style Routes with Smart Redirects

StackOverflow seems to have this style of routes for questions:
/questions/:id/*slug
Which is easy enough to achieve, both in routes and to_param.
However, StackOverflow seems to also redirect to that path when just an ID is passed.
Example:
stackoverflow.com/questions/6841333
redirects to:
stackoverflow.com/questions/6841333/why-is-subtracting-these-two-times-in-1927-giving-a-strange-result/
Same goes for any variation of the slug
stackoverflow.com/questions/6841333/some-random-stuff
Will still redirect to the same URL.
My question is: Is this type of redirection typically handled in the controller (comparing the request to the route) or is there a way to do this in routes.rb?
The reason I wouldn't think this possible in the routes.rb file is that typically, you don't have access to the object (so you couldn't get the slug based off the ID, right?)
For anyone interested, Rails 3.2.13 and also using FriendlyID
Ok, so I think I've got this.
I was looking into doing something with middleware, but then decided that's probably not the place for this type of functionality (since we need to access ActiveRecord).
So I ended up building a service object, known as a PathCheck. The service looks like this:
class PathCheck
def initialize(model, request)
#model = model
#request = request
end
# Says if we are already where we need to be
# /:id/*slug
def at_proper_path?
#request.fullpath == proper_path
end
# Returns what the proper path is
def proper_path
Rails.application.routes.url_helpers.send(path_name, #model)
end
private
def path_name
return "edit_#{model_lowercase_name}_path" if #request.filtered_parameters["action"] == "edit"
"#{model_lowercase_name}_path"
end
def model_lowercase_name
#model.class.name.underscore
end
end
This is easy enough to implement into my controller:
def show
#post = Post.find params[:post_id] || params[:id]
check_path
end
private
def check_path
path_check = PathCheck.new #post, request
redirect_to path_check.proper_path if !path_check.at_proper_path?
end
My || in my find method is because in order to maintain resourceful routes, I did something like...
resources :posts do
get '*id' => 'posts#show'
end
Which will make a routes like: /posts/:post_id/*id on top of /posts/:id
This way, the numeric id is primarily used to look up the record, if available. This allows us to loosely match /posts/12345/not-the-right-slug to be redirected to /posts/12345/the-right-slug
The service is written in a universal fashion, so I can use it in any resourceful controller. I have't found a way to break it yet, but I'm open to correction.
Resources
Railscast #398: Service Objects by Ryan Bates
This Helpful Tweet by Jared Fine

What's the correct way to run one controller action from another controller action without an HTTP redirect?

I'd like to be able to dispatch from one controller action to another conditionally, based on a combination of query parameters and data in the database.
What I have right now is something like:
class OldController < ApplicationController
def old_controller_action
if should_use_new_controller
new_params = params.dup
new_params[:controller] = "new_controller_action"
redirect_to new_params
return
end
# rest of old and busted
end
end
class NewController < ApplicationController
def new_controller_action
# new hotness
end
end
This works just fine, but it issues an HTTP redirect, which is slow. I'd like to be able to do this same thing, but within the same HTTP request.
Is there a clean way to do this?
Edit: The bounty will go to someone who can show me a clean way to do this that leaves the controllers and their actions relatively untouched (other than the redirect code itself).
Instead of calling code across actions, extract the code to lib/ or something, and call that code from both controllers.
# lib/foo.rb
module Foo
def self.bar
# ...
end
end
# posts_controller
def index
Foo.bar
end
# things_controller
def index
Foo.bar
end
Create an instance of the controller class:
#my_other_controller = MyOtherController.new
Then call methods on it:
#my_other_controller.some_method(params[:id])
I prefer the module idea, but this should do the trick.
You can also pass parameters as a whole from another controller:
#my_other_controller.params = params
I suspect you want option 3, but lets go through the some alternatives first
Option 1 - Push the controller selection logic into a helper that inserts the right link into your view. Benifits - controllers remain clean, Cons - if decision logic depending on submitted values this approach won't work. If URL is being called by external websites then this won't work.
Option 2 - Push the logic back into your model. Pro's - keeps controller clean. Cons - doesn't work well if you've got lots of sesson, params or render / redirect_to interaction.
Option 3 - Stay within the same controller. I suspect you are trying to replace some existing functionality with some new functionality, but only in some cases. Pro's - Simple and have access to everything you need. Cons - only works if it makes sense to use the same controller i.e. you're working with the same entity such as user, place or company.
Lets look an an example for option 3. My links controller has totally diferent behavour for admins than other users ...
class LinksController < ApplicationController
#...
def new
#Check params and db values to make a choice here
admin? ? new_admin : new_user
end
#...
private
def new_admin
#All of the good stuff - can use params, flash, etc
render :action => 'new_admin'
end
def new_user
#All of the good stuff - can use params, flash, etc
render :action => 'new_user'
end
end
If two controllers are trying to do the same thing, there's a very good chance this should be in a model. Take a good look at your design and -- I'm sorry I don't know your experience level with MVC -- read up on thin controller techniques:
http://weblog.jamisbuck.org/2006/10/18/skinny-controller-fat-model
http://www.robbyonrails.com/articles/2007/06/19/put-your-controllers-on-a-diet-already
http://andrzejonsoftware.blogspot.com/2008/07/mvc-how-to-write-controllers.html
If the problem is that you need the other controller to do the render, then maybe the route should have pointed there to begin with, and still the skinny controller technique should save the day.
If extracting the common code between controllers into a module doesn't work for you, I would use Rack middleware. I haven't seen code that uses ActiveRecord within middleware but I don't know of any reason why it shouldn't be possible since people have used Redis and the like.
Otherwise I think your only option would be to restart processing of the request with something like (untested, pseudo example):
env['REQUEST_URI'] = new_controller_uri_with_your_params
call(env)
This is similar to how integration tests are implemented. But I don't know if everything from call until you hit a controller is idempotent and safe to rerun like this. You could trace through the source and see. But even if it's ok now, it might break in any future version of rails or rack.
Using middleware would avoid this by letting you intercept the request before it's been run. You should still be able to share code with your rails application by extracting it out into common modules included in both places.
Honestly I think just doing the simple thing of factoring the common controller code is likely cleaner, but it's hard to know without the details of your situation so I thought I'd go ahead and suggest this.
Do this:
class OldController < ApplicationController
def old_controller_action
if should_use_new_controller
new_controller_action
end
# rest of old and busted
end
end
and the new controller
class NewController < OldController
def new_controller_action
# new hotness
end
end

Resources