Domain specific routing with different paths per domain for same model - ruby-on-rails

I have a Consultant model, where i have multiple consultant types (lawyers, doctors, psychologists etc.) listed on different websites, all handled from the same rails project.
I would like to have the consultant type as a part of the url, but have a hard time figuring out how, since it is dynamic based on domain/consultant type.
I am hoping for a solution to do a standard link:
=link_to consultant.name, consultant
without any specific link-config, so I can re-use templates across multiple consultant-websites.
Urls should be like this:
a-domain.com/doctor/doctor-name
b-domain.com/lawyer/lawyer-name
What I've tried so far, and used in the domain-specific templates (i know it is an ugly solution):
routes.rb
get 'lawyer/:slug' => 'consultants#show', as: :lawyer_consultant
get 'doctor/:slug' => 'consultants#show', as: :doctor_consultant
_consultant.html.haml for a-domain.com
= link_to consultant.name, lawyer_consultant_path(consultant)
I know the easy solution would just be this;
get 'consultant/:slug' => 'consultants#show', as: :consultant
But i want the url to be specific.
And the constraints: {host: a-domain.com} unfortunately does not allow for domain-specific routing, since only one as: :consultant can exist in routes.rb.

Routes don't actually have anything to do with your models. Your routes are the external REST API of your application while your models are an internal implementation detail.
I would just set the routes up as:
resources :doctors,
:lawyers,
only: :index
This just describes RESTful resources in your application like any other. Your routes should neither know or care that a doctor is a kind of consultant - its just a thing that can be routed to. Nor should it care that you're using slugs, to the router :id is just some kind of identifier.
The only actual connection between routes and models are the polymorphic routing helpers which basically just look up the the name of routing helper method to call based on convention over configuration:
irb(main):005:0> app.polymorphic_path("doctors") # doctors_path
=> "/doctors"
irb(main):006:0> app.polymorphic_path("doctor", id: 1) # doctor_path(1)
=> "/doctors/1"
irb(main):006:0> app.polymorphic_path(Doctor.new) # doctors_path
=> "/doctors"
irb(main):006:0> app.polymorphic_path(Doctor.find(1)) # doctor_path(1)
=> "/doctors/1"
When you pass a model instance Rails 'calls model_name.route_key on the model instance and then will determine if its singular or plural by checking if the model has been persisted.
If you want the polymorphic routing helpers to "just work" one solution is using Single Table Inheritance:
class AddTypeToConsultants < ActiveRecord::Migration[6.1]
def change
add_column :consultants, :type, :string
end
end
class Doctor < Consultant
end
class Lawyer < Consultant
end
When generating links you won't actually have to care about the type:
<%= link_to consultant.name, consultant %>
When you pass an instance of Doctor it will use doctor_path and when you pass an instance of Lawyer you get lawyer_path.
It also works for forms:
<%= form_with model: consultant do |f| %>
# ...
<% end %>
You can also acheive the same thing with the decorator pattern if STI isn't your cup of tea.
class DoctorDecorator < SimpleDelegator
def to_model
self
end
def model_name
ActiveModel::Name.new(Consultant, nil, "Doctor")
end
end
doctor = DoctorDecorator.new(Consultant.find(1))
polymorphic_path(doctor) # /doctors/1

Related

What routes are necessary when the Model and Controller names do not match?

I have a Model called Category and another called Articles. Categories are "sections" that have many Articles, for instance News and Events. Both Categories use the kind of Articles, except they're shown under a different section of my website.
Right now I'm creating the News controller (NewsController), and I'd like to visit /news/new to add News. Likewise, the same would apply to EventsController and /events/new.
What do I have to use on my routes to do this?
My first attempt was to use:
resources :categories do
resources :articles, path: '/news'
end
But this forces me to use /categories/1/news/new, which is kinda ugly.
If News will always be category_id 1 and Events will always be 2, how would I specify this on my routes, so I can easily access them with the URLs I mentioned?
Explained Differently
I have an Articles model. I'd like to have a controller called NewsController to handle Articles, so that /news/new (and the rest of the paths) would work with Article. I'd also like to have a controller called EventsController that would also handle Articles, so that /events would also work with Article. The difference between them is that they have different category_id.
Is this possible to do via routes?
Update
Made some progress.
resources :categories do
resources :articles
end
get 'news/new' => 'articles#new', defaults: {category_id: 1}
get 'events/new' => 'articles#new', defaults: {category_id: 2}
This fixes what I wanted to do with /news/new and /events/new, but I'd be missing the rest of the routes (edit, show, update, etc). Also, this makes me use the Articles controller, which currently does not exist and would also make the News controller obsolete/useless.
My logic may be wrong, it's kinda evident with what I just made, but perhaps with this update I can better illustrate what I'm trying to do.
Update 2
I'm currently testing the following:
resources :articles, path: '/news', controller: 'news'
resources :articles, path: '/events', controller: 'events'
So far it makes sense, it makes the routes I wanted, it uses both controllers with their own configurations, and it hasn't spat any errors when I visit both /news and /events (yet).
It's also possible to do:
resources :articles, path: '/news', defaults: {category_id: 1}
resources :articles, path: '/events', defaults: {category_id: 2}
But this would depend on an Article controller, which could handle both types of Categories. Either solution works (theoretically), though I'd incline more on the first since the individual controllers would allow more specific configuration to both cases. The second, though, is more adequate when there're not that many difference between the Articles being created. The defaults property isn't explicitly necessary either, I just put it there for convenience.
Your question is asking something that I question as not making sense and maybe your design is flawed.
Why would you have news resources related to category resources if they are not related?
Is categories just a name space?
If news records really are always going to be related to the same first category as your question implies then you can not use ID's as you have no control over what the id will be for the first category and the first category could have an ID of anything in which case you could just use the top level news resources and do a find first category in your model in a before create then you don't have to worry about an ugly url.
If news records really are related to categories then the you must supply the relevant category id and nest your routes but you could pretty up the url using the following from
https://gist.github.com/jcasimir/1209730
Which states the following
Friendly URLs
By default, Rails applications build URLs based on the primary key --
the id column from the database. Imagine we have a Person model and
associated controller. We have a person record for Bob Martin that has
id number 6. The URL for his show page would be:
/people/6
But, for aesthetic or SEO purposes, we want Bob's name in the URL. The
last segment, the 6 here, is called the "slug". Let's look at a few
ways to implement better slugs. Simple Approach
The simplest approach is to override the to_param method in the Person
model. Whenever we call a route helper like this:
person_path(#person)
Rails will call to_param to convert the object to a slug for the URL.
If your model does not define this method then it will use the
implementation in ActiveRecord::Base which just returns the id.
For this method to succeed, it's critical that all links use the
ActiveRecord object rather than calling id. Don't ever do this:
person_path(#person.id) # Bad!
Instead, always pass the object:
person_path(#person)
Slug Generation
Instead, in the model, we can override to_param to include a
parameterized version of the person's name:
class Person < ActiveRecord::Base def to_param
[id, name.parameterize].join("-") end end
For our user Bob Martin with id number 6, this will generate a slug
6-bob_martin. The full URL would be:
/people/6-bob-martin
The parameterize method from ActiveSupport will deal with converting
any characters that aren't valid for a URL. Object Lookup
What do we need to change about our finders? Nothing! When we call
Person.find(x), the parameter x is converted to an integer to perform
the SQL lookup. Check out how to_i deals with strings which have a mix
of letters and numbers:
"1".to_i
=> 1
"1-with-words".to_i
=> 1
"1-2345".to_i
=> 1
"6-bob-martin".to_i
=> 6
The to_i method will stop interpreting the string as soon as it hits a
non-digit. Since our implementation of to_param always has the id at
the front followed by a hyphen, it will always do lookups based on
just the id and discard the rest of the slug. Benefits / Limitations
We've added content to the slug which will improve SEO and make our
URLs more readable.
One limitation is that the users cannot manipulate the URL in any
meaningful way. Knowing the url 6-bob-martin doesn't allow you to
guess the url 7-russ-olsen, you still need to know the ID.
And the numeric ID is still in the URL. If this is something you want
to obfuscate, then the simple scheme doesn't help. Using a Non-ID
Field
Sometimes you want to get away from the ID all together and use
another attribute in the database for lookup. Imagine we have a Tag
object that has a name column. The name would be something like ruby
or rails. Link Generation
Creating links can again override to_param:
class Tag < ActiveRecord::Base validates_uniqueness_of :name
def to_param
name end end
Now when we call tag_path(#tag) we'd get a URL like /tags/ruby. Object
Lookup
The lookup is harder, though. When a request comes in to /tags/ruby
the ruby will be stored in params[:id]. A typical controller will call
Tag.find(params[:id]), essentially Tag.find("ruby"), and it will fail.
Option 1: Query Name from Controller
Instead, we can modify the controller to
Tag.find_by_name(params[:id]). It will work, but it's bad
object-oriented design. We're breaking the encapsulation of the Tag
class.
The DRY Principle says that a piece of knowledge should have a single
representation in a system. In this implementation of tags, the idea
of "A tag can be found by its name" has now been represented in the
to_param of the model and the controller lookup. That's a maintenance
headache. Option 2: Custom Finder
In our model we could define a custom finder:
class Tag < ActiveRecord::Base validates_uniqueness_of :name
def to_param
name end
def self.find_by_param(input)
find_by_name(input) end end
Then in the controller call Tag.find_by_param(params[:id]). This layer
of abstraction means that only the model knows exactly how a Tag is
converted to and from a parameter. The encapsulation is restored.
But we have to remember to use Tag.find_by_param instead of Tag.find
everywhere. Especially if you're retrofitting the friendly ID onto an
existing system, this can be a significant effort. Option 3:
Overriding Find
Instead of implementing the custom finder, we could override the find
method:
class Tag < ActiveRecord::Base #... def self.find(input)
find_by_name(input) end end
It will work when you pass in a name slug, but will break when a
numeric ID is passed in. How could we handle both?
The first temptation is to do some type switching:
class Tag < ActiveRecord::Base #... def self.find(input)
if input.is_a?(Integer)
super
else
find_by_name(input)
end end end
That'll work, but checking type is very against the Ruby ethos.
Writing is_a? should always make you ask "Is there a better way?"
Yes, based on these facts:
Databases give the id of 1 to the first record
Ruby converts strings starting with a letter to 0
class Tag < ActiveRecord::Base #... def self.find(input)
if input.to_i != 0
super
else
find_by_name(input)
end end end
Or, condensed down with a ternary:
class Tag < ActiveRecord::Base #... def self.find(input)
input.to_i == 0 ? find_by_name(input) : super end end
Our goal is achieved, but we've introduced a possible bug: if a name
starts with a digit it will look like an ID. If it's acceptable to our
business domain, we can add a validation that names cannot start with
a digit:
class Tag < ActiveRecord::Base #... validates_format_of :name,
:without => /^\d/ def self.find(input)
input.to_i == 0 ? find_by_name(input) : super end end
Now everything should work great! Using the FriendlyID Gem
Does implementing two additional methods seem like a pain? Or, more
seriously, are you going to implement this kind of functionality in
multiple models of your application? Then it might be worth checking
out the FriendlyID gem: https://github.com/norman/friendly_id Setup
The gem is just about to hit a 4.0 version. As of this writing, you
want to use the beta. In your Gemfile:
gem "friendly_id", "~> 4.0.0.beta8"
Then run bundle from the command line. Simple Usage
The minimum configuration in your model is:
class Tag < ActiveRecord::Base extend FriendlyId friendly_id :name
end
This will allow you to use the name column or the id for lookups using
find, just like we did before. Dedicated Slug
But the library does a great job of maintaining a dedicated slug
column for you. If we were dealing with articles, for instance, we
don't want to generate the slug over and over. More importantly, we'll
want to store the slug in the database to be queried directly.
The library defaults to a String column named slug. If you have that
column, you can use the :slugged option to automatically generate and
store the slug:
class Tag < ActiveRecord::Base extend FriendlyId friendly_id
:name, :use => :slugged end
Usage
You can see it in action here:
t = Tag.create(:name => "Ruby on Rails")
=> #
Tag.find 16
=> #
Tag.find "ruby-on-rails"
=> #
t.to_param
=> "ruby-on-rails"
We can use .find with an ID or the slug transparently. When the object
is converted to a parameter for links, we'll get the slug with no ID
number. We get good encapsulation, easy usage, improved SEO and easy
to read URLs.
If you are sure there will be only 2 categories, why not simply add a boolean to the articles?
Like: article.event = true if events category, false if news
Then you can add a scopes to Article class for both categories
class Article
scope :events, -> { where(event: true) }
scope :news, -> { where(event: false) }
end
Create controllers, for example:
class EventsController < ApplicationController
def index
#articles = Article.events
end
def create
#article.new(params)
#article.event = true
#article.save
end
...
end
and routes: resources :events
You should try to use dynamic segments: http://guides.rubyonrails.org/routing.html#route-globbing-and-wildcard-segments
Add some slug attribute to Category, it should be unique and add index to it.
# routes
resources :articles, except: [:index, :new]
get '*category_slug/new', to: 'articles#new'
get '*category_slug', to: 'articles#index'
# controller
class ArticlesController < ApplicationController
def index
#category = Category.find_by slug: params[:category_slug]
#articles = #category.articles
end
def new
#category = Category.find_by slug: params[:category_slug]
#article = #category.articles.build
end
...
end
Remember to put a category in a hidden field in the form_for #article

Ruby on Rails 4 find object by id

I have the following show-view, where i display basic information about Product and display other User's Products.
<h1>Book <%= #product.name %></h1>
<% #products.each do |product| %>
<ul>
<%= product.name %>
<%= link_to "Make offer", {controller: "offers", :action => 'create', id: product.id } %>
</ul>
Controller
def show
#product = current_user.products.find(params[:id])
#products = Product.all
end
My goal is to make Offer between two Products.
I created Offer model and methods for making Offers:
class Offer < ActiveRecord::Base
belongs_to :product
belongs_to :exchanger, class_name: "Product", foreign_key: "exchanger_id"
validates :product_id, :exchanger_id, presence: true
def self.request(product, exchanger)
unless product == exchanger or Offer.exists?(product, exchanger)
transaction do
create(product: product, exchanger: exchanger, status: "oczekujace")
create(product: exchanger, exchanger: product, status: "oferta")
end
end
#other methods
end
Making offers is working, because I checked it in Console.
My problem is in OffersController:
class OffersController < ApplicationController
before_filter :setup_products
def create
Offer.request(#prod, #exchanger)
redirect_to root_path
end
private
def setup_products
#prod = current_user.products.find(1)
#exchanger = Product.find_by_id(params[:id])
end
end
Problem is with a following line (using link in show-page for products with different id's than 1 works):
#prod = current_user.products.find(1)
But I don't know how to find object in Db for actual product which my show-page shows. Not only for id = 1.
I don't know how to find this object in database.
I don't know the specific answer to your question, but perhaps if I explain what you need to look at, your solution will arise:
Find
Rails isn't magic - it uses ActiveRecord (which is an ORM - Object-Relation Mapper), which means every time you fire a query (through find), your ORM (ActiveRecord) will search the relevant database data for you
The problem you have is that although you're using the correct syntax for your lookup, you may not have a record with an id of 1 in your db.
current_user.products.find(1) tells ActiveRecord to scope the query around the current user, and their products. So you'll get something like like this:
SELECT * FROM 'products' WHERE user_id = '15' AND id = '1'
Objects
Further, you have to remember that Ruby (and Rails by virtue of being built on Ruby) is an object orientated language. This means that everything you load / interact with in the language should be based on an object
The problem you have is you're not associating your object to your Rails framework correctly. What I mean here is described below, but essentially, if you build your Rails framework correctly, it will give you the ability to associate your objects with each other, allowing you to call the various products you need from your offer
This is a simplistic way of looking at it, of course. You'll want to look at this diagram to see how it works:
Bottom line - try treating your application like a series of objects, rather than a logical flow. This will help you appreciate the various associations etc that you need to get it moving forward
Resources
You mention you can't show the product on your show page for an id other than one. I think the problem is really about how to get your show action to work.
If this is the case, let me explain...
Rails is resource-based, meaning that everything you do / create needs to be centred around a resource (object) of some sort. The problem is many people don't know this, and consequently complicate their controller structure for no reason:
Above is the typical "CRUD" routing structure for Rails-based resources. This should demonstrate the way that Rails will typically be constructed -- around resources
--
Further, Rails is built on the MVC programming pattern - meaning you need to use your controller to populate a series of data objects for use in your application.
To this end, if you load a resource, and want to populate it with resourceful information of another object - you need to make sure you have set up the data objects in a way to ensure you can look them up correctly, which either means passing the data through your routes or using a persistent data-type, such as cookies or sessions
The problem you have is you need to pass the product id to your controller somehow. How I'd do that is as follows (using nested resources):
#config/routes.rb
resources :offers do
resources :products #-> domain.com/offers/2/products
end
This will give you the ability to load the products controller with the variables params[:id] for the product, and params[:offer_id] for your Offer made available:
#app/controllers/products_controller.rb
Class ProductsController < ApplicationController
def show
#offer = Offer.find params[:offer_id]
#product = Product.find params[:id]
end
end

Self nesting rails categories

I have an store application, where I need to make custom routing system where URL stores categories for products. For example, http://example.com/languages/ruby/rails will display category#show named 'rails', that has parent named 'ruby', that has parent named 'languages' and and URL of http://example.com/languages/ruby/rails/store will display product in this category.
Currently I have:
category.rb
belongs_to :parent, class_name: 'Category'
has_many :categories, foreign_key: :parent_id
has_many :products
routes.rb
resources :categories, :path => '', :only => [:index, :show] do
resources :products, :path => '', :only => [:show]
end
root :to => 'products#index'
but it still stacks up to 2, e.g. URL http://example.com and http://example.com/languages shows list of categories/subcategories, but http://example.com/languages/ruby have params: {"action"=>"show", "controller"=>"products", "category_id"=>"language", "id"=>"ruby"}
Removing products from routes does not help at all - then it just says that No route matches [GET] "/language/ruby", although I assume It might cause need for extra check if current URL point on category or product later on.
Also I tried get '*categories/:id', to: 'category#show' variations
+ I am using friendly_id gem so that path do not look like http://example.com/2/54/111/6
I just want to find out what is the best ruby on rails solution for this kind of situations, when you need search engine optimizations + endless (e.g. no way to define how deep such recursion can go) nested resources that nest themselves (including fact that category/language/category/ruby/category/rails just looks ugly).
Note: most information I used is taken from Stack Overflow and railscasts.com (including pro/revised episodes), so mentioning a good source with information like this will be great too.
I solved this myself recently with a CMS I built on Rails recently. I basically construct the routes dynamically at runtime from the database records. I wrote this blog post on the strategy:
http://codeconnoisseur.org/ramblings/creating-dynamic-routes-at-runtime-in-rails-4
The core of the solution (adapting the blog post above) is simply iterate over the database records and construct the routes needed for each category. This is the main class for doing that:
class DynamicRouter
def self.load
Website::Application.routes.draw do
Category.all.each do |cat|
get cat.route,
to: "categories#show",
defaults: { id: cat.id },
as: "#{cat.routeable_name}_#{cat.name}"
end
end
end
def self.reload
Website::Application.routes_reloader.reload!
end
end
For the above, the Category model should implement a "routeable_name" method which simply gives an underscored version of the category name that uniquely names that category's route (its not strictly necessary, but helps when doing "rake routes" to see what you have). and the #route method constructs the full route to the category. Notice the defaults which sets the ID param for the category. This makes the controller action a very simple lookup on the category's ID field like so:
class CategoryController < ApplicationController
def show
#category = Category.find(params[:id])
end
end

Rails has_one new action going to wrong route

When I click the 'New Schedule Status' button on the 'Project' show page, but the route that the error shows me is plural, when it should be singular. Here's my code:
# project.rb
class Project < ActiveRecord::Base
has_one :schedule_status
end
# schedule_status.rb
class ScheduleStatus < ActiveRecord::Base
belongs_to :project
end
# schedule_statuses_controller.rb
def new
#project = Project.find(params[:project_id])
#schedule_status = #project.build_schedule_status
end
# routes.rb
resources :projects do
resource :schedule_status
end
# _form.html.erb
<%= form_for [#project, #schedule_status] do |f| %>
...
The error informs me that my form_for line is incorrect. It seems like my instance variables are setup correctly, but the error is:
undefined method `project_schedule_statuses_path` for ...
Any idea why the route it's attempting to access is plural?
This is a bug. form_for looks for the plural version of the object. However since you've declared a singular resource :schedule_status, the path helper method is never created.
To get around this you should use :url parameter for form_for.
Look at this question/answer for more clarity.
It is not a bug it is a feature (ticket closed as won't fix):
rails issue:
https://github.com/rails/rails/issues/1769
summary quote:
the error has been around for a long while however a clean solution
doesn't readily represent itself. The polymorphic_url helper has no
'intelligence' in how it operates - it has no information about what
you've declared as resources in your routes.rb. All it has to go on it
the name of the model and how that maps to the named url helpers.
The problem is there is no easy way to discern whether a model maps
to a singular or a regular resource url. Checking for the presence of
a collection url doesn't work as the resource may have been specified
with :except => :index and trying to rescue route generation errors
doesn't work because passing an instance to a singular resource url
helper will generate a url with the format set to the id and no
exception.
rails issue closed in favour of the previous:
https://github.com/rails/rails/issues/4978
conclusion:
in such cases you're supposed to give the url. url_for cannot reflect
on routes to see if that's a resource or not.

Dynamic CMS like routes in ruby on rails

I want to create a CMS like site where the user starts off with a some generic pages, i.e.
homepage
about
contact
etc
and from there can add child pages dynamically, for example
homepage
articles
article1
something
something-else
article2
about
contact
etc
To achieve this I'm planning on using some kind of self-referential association like
class Page < ActiveRecord::Base
belongs_to :parent, :class_name => 'Page'
has_many :children, :class_name => 'Page'
end
The one thing I'm struggling with is the route generation. Because pages can be added on the fly I need to dynamically generate routes for these pages and there is no way of knowing how many levels deep a page may be nested
So if I start off with the homepage:
/
and then start adding pages i.e.
/articles/article1/something/something-else/another-thing
How can something like that be achieved with the rails routing model?
Once you have some way to generate the URL string for your Page records (and I'll leave that part up to you), you can just map every page in config/routes.rb:
Page.all.each do |page|
map.connect page.url, :controller => 'pages', :action => 'show', :id => page
end
And have an observer hook the page model to reload routes when something changes:
class PageObserver < ActiveRecord::Observer
def reload_routes(page)
ActionController::Routing::Routes.reload!
end
alias_method :after_save, :reload_routes
alias_method :after_destroy, :reload_routes
end
Don't forget to edit config/environment.rb to load the observer:
# Activate observers that should always be running
config.active_record.observers = :page_observer
One solution to this prob is to dynamically load routes from hooks on your models. From example, a snippet from the Slug model on my site:
class Slug < ActiveRecord::Base
belongs_to :navigable
validates_presence_of :name, :navigable_id
validates_uniqueness_of :name
after_save :update_route
def add_route
new_route = ActionController::Routing::Routes.builder.build(name, route_options)
ActionController::Routing::Routes.routes.insert(0, new_route)
end
def remove_route
ActionController::Routing::Routes.routes.reject! { |r| r.instance_variable_get(:#requirements)[:slug_id] == id }
end
def update_route
remove_route
add_route
end
def route_options
#route_options ||= { :controller => navigable.controller,
:action => navigable.action,
:navigable_id => navigable_id,
:slug_id => id }
end
end
This inserts the route at top priority (0 in the routing array in memory) after it has been saved.
Also, it sounds like you should be using a tree management plugin and like awesome nested set or better nested set to manage the tree for your site.
You have to parse the route yourself
map.connect '*url', :controller => 'pages', :action => 'show'
Now you should have a params[:url] available in your action that is the request path as an array separated by the slashes. Once you have those strings its a simple matter to find the models you need from there.
That was from memory, and it's been a long while. Hope it works for you.
Look at RadiantCMS sources, they implement that functionality as far as i understand their self description.
I've implemented a similar functionality into a Rails gem, using self referential associations and a tree like js interface for reordering and nesting the "pages".
Templating language and authentication/authorization are left for the developer to implement.
https://github.com/maca/tiny_cms

Resources