I've been developing the CMS backend for a website for a few weeks now. The idea is to craft everything in the backend first so that it can manage the database and information that will be displayed on the main website.
As of now, I currently have all my code setup in the normal rails MVC structure. So the users admin is /users and videos is /videos.
My plans are to take the code for this and move it to a /admin directory. So the two controllers above would need to be accessed by /admin/users and /admin/videos. I'm not sure how todo the ruote (adding the /admin as a prefix) nor am I sure about how to manage the logic. What I'm thinking of doing is setting up an additional 'middle' controller that somehow gets nested between the ApplicationControler and the targetted controller when the /admin directory is accessed. This way, any additional flags and overloaded methods can be spawned for the /admin section only (I believe I could use a filter too for this).
If that were to work, then the next issue would be separating the views logic (but that would just be renaming folders and so on).
Either I do it that way or I have two rails instances that share the MVC code between them (and I guess the database too), but I fear that would cause lots of duplication errors.
Any ideas as to how I should go about doing this?
Many thanks!
If you don't mind having two controllers for each resource, you could have a separate "admin" namespace. I like it this way, since the admin section is completely different from the public one. Admin controllers implement all CRUD actions, whereas the public ones implement only show and index actions.
routes.rb:
map.namespace :admin do |admin|
admin.resources :users
admin.resources :videos
end
map.resources :videos, :only => [:index, :show]
Your controllers could be something like:
class VideosController < PublicController; end
class Admin::VideosController < Admin::AdminController; end
class PublicController < ApplicationController
layout 'public'
before_filter :load_public_menu
end
class Admin::AdminController < ApplicationController
layout 'admin'
before_filter :login_required, :load_admin_menu
end
Namespaced controllers and views have their own subdirectory inside the app/controllers and app/views directories. If you use the form_for helper, you need to modify its parameters:
form_for [:admin, #video] do |f|
You can do this without an extra controller, relatively easily in config/routes.rb:
# non-admin routes
# your args could include :only => [:index,:show] for the non-admin routes
# if you wanted these to be read-only
map.resources :users, ...your args..., :requirements => { :is_admin => false }
map.resources :videos, ...your args..., :requirements => { :is_admin => false }
# admin routes
map.resources :users, ...your args..., :path_prefix => '/admin', \
:name_prefix => 'admin_', :requirements => { :is_admin => true }
map.resources :videos, ...your args..., :path_prefix => '/admin', \
:name_prefix => 'admin_', :requirements => { :is_admin => true }
What :requirements actually does here, because I gave it a constant and not a regex, is just to add params[:is_admin] when accessed via this route. So you can check this value in your controller, and render different views, or you can just check it in the view if the two views are similar. It's important to include the requirement with false on the non-admin versions otherwise people can just use /users/?is_admin=true.
The :name_prefix edits the route names, so you have e.g. admin_video_path(123) as well as video_path(123).
Tested on Rails 2.3.5, other versions may differ. For more about the options available on RESTful routes, see the ActionController::Resources docs.
Related
I have this in my router.rb:
namespace :me do
namespace :bills do
get :consumption_invoice, :format => :pdf
end
end
and also
resources :groups do
member do
namespace :bills do
get :consumption_invoice, :format => :pdf
end
end
end
The first one gives me:
me_bills_consumption_invoice GET /me/bills/consumption_invoice(.:format) me/bills#consumption_invoice {:format=>:pdf}
consumption_invoice_bills_group GET /groups/:id/bills/consumption_invoice(.:format) bills/groups#consumption_invoice {:format=>:pdf}
In the group, the controller called is bills/groups#consumption_invoice instead of groups/bills#consumption_invoice as I'd expect.
Why?
Thanks
EDIT
After some reflexion, here's what I'd like to achieve:
/me/bills/consumption_invoice => :controller => :bills, :action => :consumption_invoice
# and
/groups/:id/bills/consumption_invoice => :controller => :bills, :action => :consumption_invoice
Idealy, I'd like to have both those rules in the :me namespace and the :groups resource blocks for making it cleaner to read.
And I'd like to be able to add more actions easily:
/me/bills/subscription_invoice => :controller => :bills, :action => :subscription_invoice
which is why I wanted to create a block :bills in it.
I've been trying so many possibilities around, can't I achieve that?
The way paths are constructed is different than the way the controller is determined.
Controllers can be organized under a namespace. The namespace is a folder that controllers can sit in. Although the order of resources and namespaces affects the order of the parts of the path, it won't affect the way that namespaces are treated as a controller grouping. The resource is :groups so the controller is named "groups" and handles actions made against the group model. The namespace is :bills so the controller is contained within a folder called "bills".
In a similar fashion, if you nested two resources like:
resources :groups do
resources :users
end
The path for a user action would be /groups/:id/users but the controller would just be named "users".
If you really want this action to be handled by a bills controller in a groups folder, then you could customize the behavior with something like:
resources :groups do
member do
scope module: 'groups' do
get 'bills/consumption_invoice', :format => :pdf, controller: 'bills'
end
end
end
Additional Info for Edit in the Question
If you just want your two paths to reach the bills#consumption_invoice action and you don't want your bills controller to live in a folder called me, you could consider something like this:
get 'me/bills/consumption_invoice', :format => :pdf, to: 'bills#consumption_invoice'
resources :groups do
member do
get 'bills/consumption_invoice', :format => :pdf, controller: 'bills'
end
end
Rails will then expect that your consumption_invoice view lives in a bills folder.
The Rails Routing from the Outside In (http://guides.rubyonrails.org/routing.html) can provide additional details on all the routing options.
Actually I don't understand how to correctly handle this. I have a situation where news could be managed with admin/edit admin/show admin/news... and similar paths, however I want to give users a page called news/show/1, because actually my news resources are routed under "admin" namespace, how should I handle the fact that I need to bind news routes outside "admin" namespace?
Actually I have only this:
namespace :admin do
resources :news
end
My Idea:
namespace :admin do
resources :news
end
resources :news
Then I'll have:
app/controllers/admin/news_controller.rb
app/controllers/news_controller.rb
Is this correct?
Seeing your answer, I can suggest more simpler routes.
#routes.rb
namespace :admin do
resources :news
end
resources :news, :only => [:show]
If you want index action too, rewrite the last line as:
resources :news, :only => [:index, :show]
You won't need the helper for news_path and news_url. You will get them already built for you.
Ok after working a bit on routes, I understood how to build what I wanted:
namespace :admin do
resources :news
end
get 'news/:id(.:format)' => 'news#show'
This because I don't need all routes for my news, but only show (well I may add index too, but not required at the moment). In this way I can handle everything on 2 different controllers, which is better, because I use somethings like redirects on the news controller which I don't use on Admin::NewsController.
I noticed another important thing, if you build routes in this way news_path and news_url won't be created. Because of this, I had to manually create them in this way in news_helpers:
module NewsHelper
def news_url(record)
url_for controller: 'news', action: 'show', only_path: false, id: record.slug
end
def news_path(record)
url_for controller: 'news', action: 'show', only_path: true, id: record.slug
end
end
(slug is for seo-friendly urls) Then I simply included the helper in my controller in this way:
class NewsController < ApplicationController
include NewsHelper
Everything is worked as I wanted and looks great too.
I wondered if there were any plugins or methods which allow me to convert resource routes which allow me to place the controller name as a subdomain.
Examples:
map.resources :users
map.resource :account
map.resources :blog
...
example.com/users/mark
example.com/account
example.com/blog/subject
example.com/blog/subject/edit
...
#becomes
users.example.com/mark
account.example.com
blog.example.com/subject
blog.example.com/subject/edit
...
I realise I can do this with named routes but wondered if there were some way to keep my currently succinct routes.rb file.
I think that subdomain-fu plugin is exacly what you need.
With it you will be able to generate routes like
map.resources :universities,
:controller => 'education_universities',
:only => [:index, :show],
:collection => {
:all => :get,
:search => :post
},
:conditions => {:subdomain => 'education'}
This will generate the following:
education.<your_site>.<your_domain>/universities GET
education.<your_site>.<your_domain>/universities/:id GET
education.<your_site>.<your_domain>/universities/all GET
education.<your_site>.<your_domain>/universities/search POST
The best way to do it is to write a simple rack middleware library that rewrites the request headers so that your rails app gets the url you expect but from the user's point of view the url doesn't change. This way you don't have to make any changes to your rails app (or the routes file)
For example the rack lib would rewrite: users.example.com => example.com/users
This gem should do exactly that for you: http://github.com/jtrupiano/rack-rewrite
UPDATED WITH CODE EXAMPLE
Note: this is quickly written, totally untested, but should set you on the right path. Also, I haven't checked out the rack-rewrite gem, which might make this even simpler
# your rack middleware lib. stick this in you lib dir
class RewriteSubdomainToPath
def initialize(app)
#app = app
end
def call(env)
original_host = env['SERVER_NAME']
subdomain = get_subdomain(original_host)
if subdomain
new_host = get_domain(original_host)
env['PATH_INFO'] = [subdomain, env['PATH_INFO']].join('/')
env['HTTP_X_FORWARDED_HOST'] = [original_host, new_host].join(', ')
logger.info("Reroute: mapped #{original_host} => #{new_host}") if defined?(Rails.logger)
end
#app.call(env)
end
def get_subdomain
# code to find a subdomain. simple regex is probably find, but you might need to handle
# different TLD lengths for example .co.uk
# google this, there are lots of examples
end
def get_domain
# get the domain without the subdomain. same comments as above
end
end
# then in an initializer
Rails.application.config.middleware.use RewriteSubdomainToPath
This is possible without using plugins.
Given the directory structure app/controllers/portal/customers_controller.rb
And I want to be able to call URL helpers prefixed with portal, i.e new_portal_customer_url.
And the URL will only be accessible via http://portal.domain.com/customers.
Then... use this:
constraints :subdomain => 'portal' do
scope :module => 'portal', :as => 'portal', :subdomain => 'portal' do
resources :customers
end
end
As ileitch mentioned you can do this without extra gems it's really simple actually.
I have a standard fresh rails app with a fresh user scaffold and a dashboard controller for my admin so I just go:
constraints :subdomain => 'admin' do
scope :subdomain => 'admin' do
resources :users
root :to => "dashboard#index"
end
end
So this goes from this:
site.com/users
to this :
admin.site.com/users
you can include another root :to => "{controller}#{action}" outside of that constraint and scope for site.com which could be say a pages controller. That would get you this:
constraints :subdomain => 'admin' do
scope :subdomain => 'admin' do
resources :users
root :to => "dashboard#index"
end
end
root :to => "pages#index"
This will then resolve:
site.com
admin.site.com
admin.site.com/users
Ryan Bates covers this in his Railscast, Subdomains.
The following question is related to passing a variable from routes to the controller. I have a Ruby on Rails (v 2.3.3) app with one model, which is delivered to the user with multiple views. The current solution involves using multiple controllers which are triggered by multiple routes. For example:
ActionController::Routing::Routes.draw do |map| # defines map
map.resource :simpsons, :only => [] do |b|
b.resources :episodes, :controller => "SimpsonsEpisodes"
end
map.resource :flintstones, :only => [] do |b|
b.resources :episodes, :controller => "FlintstonesEpisodes"
end
However, for the sake of DRYness I would like these routes to operate with the same controller. In order for the controller to distinct between the routes I would like to pass along a variable via the route. For example:
map.resource :simpsons, :only => [] do |b|
b.resources :episodes, :controller => "Episodes", :type => "simpsons"
end
map.resource :flintstones, :only => [] do |b|
b.resources :episodes, :controller => "Episodes", :type => "flintstones"
end
So in the controller I could do this:
case(type)
when "simpsons" then ... do something for the Simpsons ...
when "flintstones" then ... do something for the Simpsons ...
else .... do something for all episodes ....
end
I found a way to do this with non-RESTful routing (map.with_options etc.), but I'd prefer to use RESTful routes with map.resource(s). One ugly solution might be to parse the request URI in the controller, which I'd not prefer.
Since there are no replies and I found a solution, I am going to answer this question myself. If you also have a situation, where you might have a model or a table, which has multiple entries, but for some of them you might need separate views, this might benefit you a bit.
Most likely you would like to have different indexes and for that simply use collection get:
map.resources :episodes, :collection => {:simpsons=> :get, :flintstones=> :get} do |episode|
....and so on
Simply add the methods named "simpsons" and "flintstones" in your controller. And, for the edit, view and delete methods you can use a bit of extra logic, if necessary, by determening the ID of the entry at hand. Something like #episode = Episode.find(params[:id]), if #episode.criteria == ...something... then render this or the other view.
I have a site listing many jobs, but I also want each account to be able to access its jobs in one place. Thus, I am using these routes:
map.resources :jobs
map.resource :account, :has_many => :jobs
This gets me URLs like localhost/jobs/ and localhost/account/jobs. However, both seem to render JobsController::index. How can I either make a conditional in the index action (how do I access whether account/jobs or just jobs was specified in the URL?) or change the account route to render a different action? What's the proper way to do this?
You can use a block when creating your routes, and then pass a :controller parameter, like so
map.resource :account do |account|
# If you have a special controller 'AccountJobsController'
account.resources :jobs, :controller => "account_jobs"
end
It may be cleaner for you to put your controllers into a directory structure, and then you can reference them in a nested way. For example:
map.resource :account do |account|
account.resources :jobs, :controller => "accounts/jobs"
end
If you use the above snippet, you should then create a controller in app/controllers/accounts/jobs_controller.rb, which is defined like so:
class Account::JobsController < ApplicationController
##
## etc.
##
end
You can always use rake routes to check which routes have been generated and which controllers they'll use.
Adding a requirement to the resource definition allows you to pass extra parameters
map.resources :jobs
map.resource :account, :has_many => :jobs, :requirements => {:account => true}
Then params[:account] will be set if the routing url was 'http://www.mysite.tld/account/jobs' and unset if it it was 'http://www.mysite.tld/jobs'
As with all other restful routing the action depends on the context.
GET requests without an id route to index.
GET requests with an id route to show
POST requests route to create
PUT requests route to update
DELETE requests route to destroy.
If you run "rake routes" you should see something like this
account_jobs GET /accounts/:account_id/jobs/:job_id {:controller => 'jobs', :action => 'index'}
This means when your action is called via the /account/jobs route you should have an :account_id parameter. You can then do your logic switch based on the existence of this param:
if params[:account_id].nil?
...
else
...
end