I have multiple user types in a system that shows each user different views and templates of the stored information, often based on whether they are logged in and what current_user.user_type is. As a result, I have lots of this:
#controller
#project = Project.find(params[:id])
if current_user.user_type == "Company"
redirect_to :controller => "companies", :action => "home"
elsif current_user.user_type == "Contractor"
#contractor = Contractor.find(current_user.user_type_id)
redirect_to :controller => "contractors", :action => "home"
elsif current_user.user_type == "Customer"
redirect_to :controller => "companies", :action => "list"
end
This is my first Rails project and I am pretty sure this is poor design. What are simple clean ways of doing this in a better way?
If you're having a lot of code like this, is a code smell that your controller is really serving multiple purposes. Assuming your controller is something like InfoController, which is a REST view for some Info model, ask yourself:
What is the central piece of your actions, the data or the user who access
it?
Are you taking decisions based on who
requests the actions? (Like saving,
deleting, etc)
Can these decisions be taken in the
Info model, rather than in the controller?
To me, seems like you should create different controllers for each model, and make just one redirect per action. In your views, you can use things like polymorphic_paths to link up to your controllers.
If you decide not to do this, I'd just put that code in a case statement instead of an if.
Can also use constraints as seen here: User-centric Routing in Rails 3
root :to => "companies#home", :constraints => UserTypeConstraint.new("Company")
Or scoped routes as seen here: Use lambdas for Rails 3 Route Constraints
scope :constraints => lambda{|req| !req.session[:company_user_id].blank? } do
# all company routes
end
When a user hit login in your application, you must have some code to redirect them somewhere.
This is where you want to put your code.
and you probably want to use:
redirect_to companies_path
Related
I am developing a app in ruby on rails for a local business. The pages are 'static', but changeable through a backend CMS I am building for them. Is there a best practice to creating a controller for static pages? Right now I have a sites controller with all static routes, like this.
routes.rb
get "site/home"
get "site/about_us"
get "site/faq"
get "site/discounts"
get "site/services"
get "site/contact_us"
get "site/admin"
get "site/posts"
or would I be better off creating member routes for the site controller like this without the crud, because a 'Site' will not need to have the CRUD.
resources :sites, :except => [:index, :new, :create, :update, :destroy]
member do
get :home
get :about_us
get :faq
get :discounts
get :services
get :contact_us
get :admin
get :posts
end
Or is there a best practice / better way? Any answers would be appreciated. Thanks
If the static pages list are not going to increase, then you can keep the list, but if you want a dynamic list like site/any_new_url , save the routes as
get 'site/:cms_page' => 'cms#show' # all requests matching site/any_page will go CmsController, show method
This will help reduce keep the routes from bloating, but the downside is you do not know what all routes are the valid ones. Your sample code can be
def show
#page_data = Page.find_by_page(:params[:cms_page])
end
show.html.erb
<%= #page_data.html_safe %>
Dunno yet if I consider this a best practice or an abomination but here is what I came up with when tackling the same problem.
My reasoning is that the site was providing some specified functionality (which doesn't really matter for this discussion) + a bunch of information about the organisation itself (about us, contact, FAQ, homepage blurb, whatever). Since all that data was really related to the organisation, an Organisation model seemed reasonable with each of those things as attributes. Here is the model:
class Organisation < ActiveRecord::Base
...validations stuff...
def self.attrs_regex
Regexp.new(self.attrs.join("|"))
end
def self.attrs
self.column_names.reject{|name| name =~ /id|created_at|updated_at/}
end
end
Then I use the attrs class method to generate routes based on the columns. This is in my routes.rb:
Organisation.attrs.each do |attr|
get "#{attr}" => "organisation##{attr}", :as => attr.to_sym
get "#{attr}/edit" => "organisation#edit", :as => "#{attr}_edit".to_sym, :defaults => { :attribute => attr }
post "#{attr}" => "organisation#update", :as => :organisation_update, :defaults => { :attribute => attr}, :constraints => Organisation.attrs_regex
end
The controller gets a little weird and I am not thrilled with the code here but here it is anyway. I need to make sure the attribute is set and available to the views so I can do the right thing there so I set it in the application controller:
class ApplicationController < ActionController::Base
protect_from_forgery
before_filter :set_attribute
def set_attribute
#attribute = action_name.parameterize
end
end
For the organisation controller I just set the #organisation variable to be the first and only row in the database in the before_filter and then let Rails do its usual magic of calling the method, failing, and rendering a view of the same name. The edit action just uses one view file to edit all the different attributes:
class OrganisationController < ApplicationController
before_filter :set_organisation
def edit
authorize! :edit, #organisation
#attribute = params[:attribute].parameterize
end
def update
authorize! :update, #organisation
#attribute = params[:attribute]
respond_to do |format|
if #organisation.update_attributes(params[:organisation])
format.html do
redirect_to "/#{#attribute}", notice: t('successful_update')
end
format.json { head :ok }
else
format.html { render action: "edit" }
end
end
end
private
def set_organisation
#organisation = Organisation.first
end
end
So that is where I ended up. Like you I hit up SO to tap into the seething mass of genius here but ended up with disappointing results. If there is something better out there I am still hoping to find it.
What I like about what I did is that routes are automatically generated based on the structure of the organisation table.
What I don't like about what I did is that routes automatically generated based on the structure of the organisation table.
I know I will pay for that design decision when I have to deal with i18n routing and there are probably a thousand other reasons that this is a bad idea that I have yet to discover but for the moment I have a happy client.
In the end this is not a suggestion that you should do this, but I am hoping to give you more than I got so you can advance your thinking on this and hopefully end up a little closer to that best practice.
If you are going to construct a CMS, which likely connects to a database, and allow your customer to change the text on the pages of their site, I would not recommend using static pages. In Rails terms, a static page would refer to creating html files in your /views/pages directory. If you go this route, then you're walking outside of the way that Rails was designed.
I believe that what you want to do is create tables in the database that correspond to and store the data for your posts, etc. You can pull information into the controller from the model that it corresponds to and then user a view to display the data. You can create a layout for these pages and then create controllers for each of the pages that you add.
As far as routes, I would recommend using the following:
map.resource :controller_name
you then would add the code that displays the information from the CMS in the corresponding show controller action and view for each page.
I am using AuthLogic to authenticate users in my rails app. That part is set up and workign properly.
I have the following route defined:
map.login '/account/login', :controller => :user_sessions, :action => :new
Calling rake routes returns what I expect:
login /account/login {:controller=>"user_sessions", :action=>"new"}
When someone submits a login, it calls UserSessionsController.create:
def create
#user_session = UserSession.new(params[:user_session])
if #user_session.save
flash[:notice] = "Login successful!"
redirect_back_or_default account_url
else
render :action => :new
end
end
If #user_session.save fails, the appropriate error messages appear on the screen. However, the browser URL also changes to "http://localhost:3000/user_session" instead of staying on "http://localhost:3000/account/login".
I assume the problem is what I am feeding to the render method. What should I be feeding it?
This is actually the intended behavior for this process. In a standard scaffolded RESTful controller, a validation error in the create and update actions will simply render the original template without redirecting. This results in what you are seeing – the new template will be displayed with the create action's URL in the URL bar. The reason for this is that in order to display information to the user about what errors occurred, the view must have access to the invalid model object, which is #user_session in your case.
You can use redirect_to instead of render if you want to force a redirect to the original URL, but this will cause you to lose information about the errors. You would need to manually persist the errors in the session, which would be messy. My advice is not to worry about the fact that the URL doesn't match that of the original as this is pretty standard in all Rails apps.
Just adding solution for Rails 4 (based on Shaun's answer here):
Add new route to routes file:
post '/carts/new' => 'carts#create', as: :create_post
Add url: create_post_path to form tag
Done.
After further digging, I found the solution in another StackOverflow question: Use custom route upon model validation failure
I simply modified my routes to add a new one for posing to '/account/login':
map.login '/account/login', :controller => :user_sessions, :action => :new, :conditions => {:method => :get}
map.login_post '/account/login', :controller => :user_sessions, :action => :create, :conditions => {:method => :post}
Then, I updated my view to utilize the new route:
<% form_for #user_session, :url => login_post_path do |f| %>
This works perfectly. A failed login gives the appropriate error messages and maintains the '/account/login' URL.
First, an example of what I'm trying to do:
If you go to http://www.meetup.com and you are not signed in, you see a page that shows 'Do something • Learn something...' etc. which is a public page
But when you are logged in, that same page (URL) shows 'Welcome, xxx...Whats happening...' etc. which is specific to you.That is what I'm trying to do in my app as well.
How to go about this in Rails 2.3.8?
So far, I have:
An AboutsController intended to serve up semi-static pages (wish the About wasn't plural!)
Root route is map.root => :controller => "about".
Now, when a non-logged-in-user goes to the http://www.abc.com, he would get the contents of the view about/index. So far so good.
But, when a user is logged in, I want that the products/index view should be displayed for the same URL i.e. http://www.example.com URL (and not http://www.example.com/products)
Is this possible in Rails 2.3.8?
The cleanest way to do this is with something called a before_filter. It's a command at the top of your controller that is called before any action in that controller (that you want). Normally, you'll want to do the same check for more than one action in the controller, so it doesn't make sense to put that check into each action directly.
Let's say you have a Comments controller, and you want the edit, update, and destroy actions to be something only a logged in user can do. This is very common. Let's look at an example. For the sake of brevity I won't spell out all the controller actions, just the unique stuff:
class CommentsController < ApplicationController
before_filter :user_logged_in?, :only => [:edit, :update, :destroy]
# all your actions would go here - index, show, etc #
protected
def user_logged_in?
redirect_to dashboard_path unless current_user
end
end
In the example above, the user_logged_in? method is going to run before the edit, update, or destroy actions are run. If a render or redirect is called inside that method, rails will stop short and never run the action That's why it's called a filter. Instead will honor the render or redirect request given.
The current_user method is a common helper that most user authentication plugins give you, which is usually nil if there is no current user. So our filter method is telling rails to redirect to a certain path, unless the user is logged in.
This is the de facto rails way to handle something like this. Very good question.
It's certainly possible. You can render whichever views you need conditionally like this:
def index
if current_user
render :action => 'products', :controller => 'index'
else
render :action => 'index', :controller => 'about'
end
end
Assuming that you're authenticating with Authlogic, Devise. Whatever logic you use to determine if a user is logged in would go into the conditional.
I've seen a lot of websites handle this with a redirect to, say, /dashboard, to keep their application as clean internally as possible. No need to get too worked up about it still being the root URL, though it's distinctly possible, as the other solutions indicate :)
I am looking for a way to decide the routes based on a request parameter.For example i want to have route a request to web controller if it has params[:web] and to iPhone if it has params[:iphone]. Is it possible to do so keeping the names of the routes same but routing them to different controllers/actions depending upon the parameter?
Possible if you define route(or a named route) like below in your routes.rb file
map.connect '/:controller/:action/:platform',:controller => 'some controller name',:action=>'some action'
if you handle this in your action, you can use like params[:platform]
Read more on named routes if you customize more on this. As far as your prob is concerned I hope the above code solves the problem
Expanding on #lakshmanan's answer, you can do this:
Add this to your routes.rb:
map.named_route '/:controller/:action/:platform'
In your views,
<%= link_to "Blah blah", named_route_path(:controller => "some_controller",
:action => "some_action",
:platform => "some_platform")
In your some_controller,
def some_action
if params[:platform] == "web"
#DO SOMETHING
elsif params[:platform] == "iphone"
#DO SOMETHING
else
#DO SOMETHING
end
end
Assuming that there is a very good reason to have one controller accept this action (if there is shared code... move it to a helper method or model, and use the user agent info or named routes to your advantage), check the parameter and redirect to the appropriate controller and action:
def some_action
# some shared code here
if params[:platform] == 'iphone'
redirect_to :controller => 'foo', :action => 'bar'
elsif params[:platform] == 'web'
redirect_to :controller => 'baz', :action => 'baq'
else
# default controller and action here
end
end
If you really really want the named route to map to different controllers, you'll need to hardcode the platform string:
map.connect '/foo/bars/:id/iphone', :controller => 'iphone',:action=>'some_action'
map.connect '/foo/bars/:id/web', :controller => 'web',:action=>'some_action'
UPDATE0
From here, you might want to try map.with_options(:conditions => ... )
In Ruby on Rails, is it possible to change a default action for a RESTful resource, so than when someone, for example, goes to /books it gets :new instead of the listing (I don't care if that means not being able to show the listing anymore)?
I'd point out that if you are pointing /books to /books/new, you are going to be confusing anyone who is expecting REST. If you aren't working alone, or if you are and have other come on board later, or if you expect to expose an API to the outside, the REST convention is that /books takes you to a listing, /books/new is where you create a new record.
Not sure why would you do such a thing, but just add this
map.connect "/books", :controller => "books", :action => "new", :conditions => { :method => :get}
to your config/routes.rb before the
map.resources :books
and it should work.
Yes. You should be able to replace your index method in your controller...
def index
#resource = Resource.new
# have your index template with they proper form
end
In the same vein, you can just do
def index
show
end
def index
redirect_to new_book_path
end
I think would be the simplest way.