Rails routing: Scope using a database field - ruby-on-rails

I am creating an multitenant app based on ideas from Ryan Bigg's book "Multitenancy with Rails". In this book, the tenants has their own subdomain. This approach is not applicable in my case, so I'm trying to scope by a slug of the account's name instead.
So instead of URLs like http://account-name.myapp.com, i want http://myapp.mydomain.com/account-name/. The subdomain is reserved for the app itself, because I want to be able to have more than one app on my domain.
Here's a piece of my routes.rb:
scope module: 'accounts' do
resources :customers do
resources :notes
end
end
To achieve my goal, i try to follow the routing guide on rubyonrails.com (the last code snippet in chapter 4.5), and change the above code to:
scope ':slug', module: 'accounts' do
resources :customers do
resources :notes
end
end
slug is an attribute in the accounts table in the database, so if an account is called "My Business", the slug will typically be "my-business".
This change seems to correct my routes:
customers GET /:slug/customers(.:format)
.. but it also seems to break my site, as the slug is not fetched from the database. I can't seem to wrap my mind around how this scope':slug', module: 'accounts' works. Is Rails supposed to automatically recognize :slug as an attribute of the Accoounts table? If not, can anyone please help me find a way to use the account's slug in my URLs?
I have googled around for a couple of days now, and read numerous answers here on Stackoverflow. Nothing helped, so any pointers is greatly appreciated. :-)
EDIT:
The relevant controllers are set up like this:
controllers/accounts/base_controller.rb
controllers/accounts/customers_controller.rb
controllers/accounts/products_controlelr.rb
controllers/accounts_controller.rb
controllers/application_controller.rb
The accounts_controller.rb only has actions for new and create at this point.
The accounts/base_controller.rb look like this:
module Accounts
class BaseController < ApplicationController
before_action :authorize_user!
def current_account
#current_account ||= Account.find_by_slug(params[:slug])
end
...
end
end
I addded this to my Account model:
def to_param
slug
end
Before i tried to implement scope ':slug' in my routes, everyting worked when logged in users where directed to myapp.mydomain.com/dashboard and navigated to i.e. myapp.mydomain.com/customers. Now it works with myapp.mydomain.com/account-name/dashboard, but as soon as I try to navigate to a view that use helpers like new_customer_path, i get the error:
No route matches {:action=>"show", :controller=>"accounts/customers", :id=>nil, :slug=>#
I hope this makes my issue clearer. :-)

I am not sure whether your routes is set up correctly or not because you didn't post your controller source code, but basically how it works if very simple. If you are using the current routes you set up what you should do is create an account_customers_controller.rb file in controllers\account\folder, and it should look like this:
class Accounts::CustomersController < ActionController::Base
def show
#account = Account.find_by_slug(params[:slug])
...
end
end

Related

Rails 4. Different routing for few models (relatd and not)

I am now working on website SEO optimization and what I am required to do is proper routing for links to be very seo friendly. I have read lots of information about routing, but it messed up in my head and I stuck.
So I have Store model which belongs to StoreType model, to City model and District model + District belongs_to :city.
I need to have routes like this:
/stores/store_type_name/ - store_type 'show' action(list of stores by type)
/stores/city_name/store_type_name/ - store_type 'show' action(list of stores by city&type)
/stores/city_name/district_name/store_type_name/ - store_type 'show' action(list of stores by city&district&type)
/stores/city_name/store_type_name/store_name - store 'show' action
The only solution I came up with for now is:
Routes.rb
namespace :stores do
get ':transliterated', to: 'store_types#show'
get ':transliterated/:name_en', to: 'store_types#city'
get ':transliterated/:name_en/:id', to: 'store_types#district'
end
With controller like this:
def district
#store_type = StorerType.find_by_transliterated(params[:transliterated])
#city = City.find_by_name_en(params[:name_en])
#district = District.find_by_id(params[:id])
if #store_type && #city && #district
stores = #store_type.stores.where(city_id:#city.id)
#stores = stores.where(district_id:#district.id)
else
redirect_to root_path
end
end
That works well but 1) I can not now add route for last example(store show page) as route is looking for :transliterated params in that namespace and redirects if record is not found. 2) I understand that this solution is bad and can be done much better, I just do not know how. Give me an advice please.
PS. Actually there is routing implemented on the site already so I am looking for the solution for those 4 urls listed above only, without touching anything else there.
Resourceful
Firstly, let me define the basis of all your routing for you...
Rails' routing structure is known as being resourceful - meaning based around resources / objects. As with Ruby being an object-orientated language, Rails is an object-orientated framework; the routes are no exception to this:
This means anything you do with your routes has to be resource-based, as follows:
#config/routes.rb
namespace :stores do
resources :store_types, only: [:show], path: "" do #-> domain.com/stores/:id -> store_types#show
get :name_en, action: :city #-> domain.com/stores/:store_type_id/:name_en
get :name_en/:id, action: :district #-> domain.com/stores/:store_type_id/:name_en/:id
end
end
This will give you the ability to send the traffic directly to your store_types controller without having all sorts of crazy routes all over the place
--
friendly_id
Something else to consider is a gem called friendly_id
friendly_id basically allows you to define / call routes with slugs, rather than ids. The difference is that the routes remain the same - it's the data, and the handling of that data, which changes
Typically in Rails, you'll create routes like this: domain.com/controller/:id
When you send people to links, they'll hit domain.com/controller/1 for example. Friendly_ID basically facilities the ability to send people to domain.com/controller/your_name, handling it in exactly the same way as you would with an ID:
#app/models/your_model.rb
Class YourModel < ActiveRecord::Base
friendly_id :name, use: [:slugged, :finders]
end
This will allow you to call:
#app/controllers/your_controller.rb
Class YourController < ApplicationController
def show
#model = Model.find params[:id]
end
end
You can use some static strings in the urls to help to identify the actions, for examples:
namespace :stores do
get 'type/:transliterated', to: 'store_types#show'
get 'type/:transliterated/city/:name_en', to: 'store_types#city'
get ':transliterated/:name_en/:id', to: 'store_types#district'
end

Accessing a Module Resource with Rails 4.x

I am using rails 4.1 with Casein CMS: https://github.com/russellquinn/casein
I have setup a Post Model, view and controllers within casein, but I would like to access the Posts outside of casein, possibly under another route called blog
I have tried and tried reworking my routes and controllers, and have an array of errors to list. Someone here might know just the trick to get this working, and was hoping some could help me, or at least explain to me what should be happening or what I might be doing wrong.
What Casein adds to the routes is this:
#Casein routes
namespace :casein do
resources :posts
end
And I'd like to match the index and show actions to => /blog. How might I write this correctly in my routes.rb.
My controller, I have basically extracted the actions from the Casein's PostsController, and along with including the Casein Module have tried to simple list all the posts.
Here is what my blogs_controller's index action looks like:
class BlogsController < ApplicationController
module Casein
def index
#casein_page_title = 'Posts'
#posts = Post.order(sort_order(:title)).paginate :page => params[:page]
end
end
end
By the end I'd also like to take blogs to blog, but I think can take it from there, but if anyone has any suggestions, that would be much appreciated.
You might be asking for this, but your question is not very clear.
If you want to have the following routes and use the same controller for each.
Prefix Verb URI Pattern Controller#Action
casein_posts GET /casein/posts(.:format) casein/posts#index
POST /casein/posts(.:format) casein/posts#create
new_casein_post GET /casein/posts/new(.:format) casein/posts#new
edit_casein_post GET /casein/posts/:id/edit(.:format) casein/posts#edit
casein_post GET /casein/posts/:id(.:format) casein/posts#show
PATCH /casein/posts/:id(.:format) casein/posts#update
PUT /casein/posts/:id(.:format) casein/posts#update
DELETE /casein/posts/:id(.:format) casein/posts#destroy
blog GET /blog(.:format) casein/posts#index
GET /blog/:id(.:format) casein/posts#show
then your config/routes.rb file should contain
namespace :casein do
resources :posts
end
get '/blog', to: 'casein/posts#index'
get '/blog/:id', to: 'casein/posts#show'
And you need your controller to be app/controllers/casein/posts_controller.rb
But I'd really strongly encourage you to use 2 different controllers, and a concern for the shared methods

Possible to Change Rails Routing Convention?

I'm wondering if it's possible to edit the default Rails routing convention to fetch a specific record based on a field that is not the ID?
For instance, instead of retrieving a specific record based on ID, with the verb/url combination:
GET /users/:id
Retrieve a specific record based on username, with the verb/url combination:
GET /users/:username
I don't see why this would be a problem theoretically, as long as usernames were required to be unique, but I'm having trouble understanding how to implement it based on the Rails Routing Guide.
I have gathered that I will need to add a line to my routes.rb file, to define a singular resource, just prior to:
resources :users
However, I'm having trouble understanding the syntax to accomplish this. Any help in understanding this would be greatly appreciated.
Yes it is possible and they are called Non Restful Routes in the rails documentation
A trivial example is doing the below in your routes.rb
get ':users/:show/:username', controller: "users", action: "show"
and in your UsersController you have a show action that looks like this:
def show
if params[:id].present?
#user = User.find(params[:id])
elsif params[:username].present?
#user = User.find_by(username: params[:username])
end
end
This way you support showing by id and username, if you want do disable support for either of them, modify the if clause as you wish
I think you are looking to change the to_param method like so:
class User < ActiveRecord::Base
def to_param
"#{id} #{name}".parameterize
end
end
This would give the url as: /user/id-name. If you want to get rid of the id before the name it gets a little more complicated. If you were just to remove it, it will more than likely break since ActiveRecord needs the id first for finds.
To get around this I would suggest using FriendlyId gem: https://github.com/norman/friendly_id
There is also a RailsCast showing how to use Friendly_id but its pretty straight forward.
The routes does not care if it is an ID or username.
It is really how you find it in the controller.
Just in the user show controller:
def show
#user = User.find_by_username params[:id]
end

Obtaining ID of containing resource via params[:id] for custom actions

I have the following routes in my config/routes.rb file:
resources :employees do
get 'dashboard'
get 'orientation'
end
employees refers to a regular resource handling the standard RESTful actions. dashboard and orientation are what I currently refer to "custom actions" which act on Employee instances. I apologize if I have my terminology mixed up and dashboard and orientation are really something else. These custom actions respond to URLs as follows:
http://myhost/employees/1/dashboard
i.e. They're "member" actions much like show, edit etc.
Anyway, this all works well enough. Regular actions such as show on EmployeesController obtain the ID of the associated Employee through params[:id]. However, with this current structure, dashboard and orientation have to use params[:employee_id] instead. This is not too difficult to deal with, but does lead to some additional code complexity as my regular before_filters which expect params[:id] don't work for these two actions.
How do I have the routing system populate params[:id] with the ID for these custom actions in the same way as show etc.? I've tried various approaches with member instead of get for these actions but haven't got anything to work the way I would like yet. This app is built using Ruby on Rails 3.2.
This might help you:
resources :employees do
member do
get 'dashboard'
get 'orientation'
end
end
and the above will generate routes like below, and then you will be able to use params[:id] in your EmployeesController.
dashboard_employee GET /employees/:id/dashboard(.:format) employees#dashboard
orientation_employee GET /employees/:id/orientation(.:format) employees#orientation
I haven't tested this example, but you can set the resourceful paths explicitly.
Something like this might work:
resources :employees, path: '/employees/:id' do
get 'dashboard', path: '/dashboard'
get 'orientation', path: '/orientation'
end

Alias route's name

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.

Resources