I'm pondering over what would be the best solution for the following scenario:
We have a basic website which everybody can navigate without sign up/sign in. The controllers and views live under app/controllers and app/views/model_name.
When the user signs up and logs in (with devise) she should be redirected to a different controller under app/users/ and correspondingly to different views under app/views/users/.
I know, I could render different partials based on signed_in? from my views and I could also redirect from the default controller to the controller nested under the user directory, but I feel there must a more general approach.
I'm wondering if it could be done with routing alone (and of course having the directory structure with the different controller/view pairs in place).
Example:
app/
controllers/
albums_controller.rb
users/
albums_controller.rb
views/
albums/
index.rb
users/
albums/
index.rb
Edit: I rewrote this post being a little bit more specific and got pointed towards the solution which consisted in overriding after_sign_in_path_for.
I can think of two ways:
Combine a redirection with a custom constraint
Create a sub-class of ApplicationController:
class SignedOutApplicationController < ApplicationController
before_action :redirect_if_signed_in
...
end
and make it the sub-class for all of the non-user controllers. For symmetry I would create a SignedInController
Related
I have a logic question and I cannot figure out how to do it. First of all, I am working on an social networking site, and I completed the site in pure PHP, but now I am re-writing the backend in rails.
My questions is, I generated UsersController, it has new, create, show, edit, update, delete, and destroy.
I thought I could use "new" to display sign up page, "create" to process sign up, "show" to display profile page, "edit" to display account settings and "update" to process edit.
I might have a logic problem here, maybe I should put "new" and "create" in a signup controller. This where I get confused. The problem with the first logic I said, I have 2 layouts, one of them is for before login, and the other one is for after login. (You can imagine Facebook's header before login, and after login).
So, when I have 2 different layout, I cannot use 1 controller in 2 layout. Because sign up page has "before login header design" and account settings and profile has "after login header design". And as you can guess I define layout in controller.
I don't know if I explained well. Thank you.
By default, Rails will look-up a layout with the same name as the controller, or else application.html.erb. But you can also specify one controller-wide (which won't help you, but bear with me)
class SomethingController
layout "some_name"
...
That's still layout-wide, so not what you need.
But you can also specify a specific layout on each call to render in an action:
def edit
#some logic
render "some_template", :layout => "some_layout"
end
Or, to take the default template lookup, but still specify a layout:
def edit
# some logic
render :layout => "some_layout"
end
There's another way you can specify layouts too, which might be especially appropriate for the use case of "one layout if not logged in, another if logged in":
class SomeController
layout :method_name_to_determine_layout
# ... actions ...
protected
def method_name_to_determine_layout
if current_user
"logged_in_layout_name"
else
"not_logged_in_layout_name"
end
end
You can learn more about the different ways to specify layouts in the Layouts and Rendering Rails Guide
Hope this helps.
Rails has basic CRUD default actions. Additionally each action can have different processing depending on the HTTP verb. You can also add custom actions & routes.
It is best to follow standard Rails practices for each default action. For example, "new" action should route to the form to create a new user when accessed via GET. An HTTP POST to the form should route to the "create" action.
If you need to add an additional controller action, do so with a custom method. Again, I stress, simple CRUD actions should follow normal Rails conventions.
Read more about routing
Read this guide many times to understand simple CRUD actions in Rails
Instead of using 1 controller in 2 layouts, I decided to use separate controllers. So, I have profile_controller, which has "edit" and "update" for account settings and "show" to display profile. And I also users_controller, which has followings: login, login_attempt, signup, signup_attempt, etc..
So, I am not putting signup and edit together in 1 controller, instead using 2 different controllers is much better and clean, I guess.
Sounds like you're trying to roll your own authentication.
I'd recommend using Devise... great tutorial here:
The reason for this is two-fold.
Firstly, Devise gives you the ability to split your app between authenticated and non-authenticated users. Namely, it provides the user_signed_in?, devise_controller? and current_user helpers to aid with this.
This might not appear like a big deal, but it will actually help you with your layouts (I'll describe more in a second).
Secondly, Devise is pre-rolled. Your questions about how to handle signups and registrations have already been solved. Of course, there's nothing preventing you from making your own authentication (Devise is just built on Warden after all), but it should give you some ideas on how this has been done already.
In regards your original question (about layouts), the other answer is very good (in terms of setting layouts per method etc).
To add to it, I would say that you have to remember that Rails is a series of classes. As such, setting the layout option in the controller is the best way to ensure you're getting the correct one.
Here's Rails explanation on it:
#app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
layout :your_layout
private
def your_layout
devise_controller? || !user_signed_in? ? "new_user" : "application"
end
end
I've found it better to keep your logic as terse as possible. IE only have one set of logic for the layout. We tend to keep it in the ApplicationController, overriding where necessary.
--
Finally, your questions also highlighted a base misunderstanding of Rails. I'm not being disrespectful; I've found the clearer your perception of the framework, the better you can work with it:
You have to remember several key points with Rails:
It's an MVC (Model View Controller) framework
It's built on Ruby; hence is object orientated
maybe I should put "new" and "create" in a signup controller. This where I get confused.
If you take Devise as a model, you'll see that you could treat your controllers as layers of abstraction for objects. That is, as Devise shows us, you can have sessions and registrations controllers.
I'm not advocating you do this exactly, I am trying to show that if you put the focus onto the objects you're working with, it becomes clearer where your controller actions should be placed.
You'll also be best understanding the CRUD (Create Read Update Destroy) nature of Rails controllers & objects. The standard Rails controller is set up as such:
Whilst this is not strict, it does give you another indication as to the structure your controllers should adhere to.
You may know this stuff already!
I have a user model that differentiates into 6 roles, and I am defining different variables and directing them to different view files (similar format, but different tables of information), but within the same controller action, because they all have similar pages "overview", "profile", "messages", etc.
Now the controller is really messy, and has multiple if/else statements. I believe I should be changing the routes so that each user has its own controller, eliminating the use of the if/else monstrosity that currently invades the controller.
def index
if current_user.admin?
....
end
if current_user.moderator?
....
end
end
Question: How do I perform the routing such that url will be
www.website.com/1/schedule, where 1 = current_user.id, while having different view files rendered from the different controllers?
I am thinking of doing a AdminController and a ModeratorController to handle this, but am not sure how to do the routing, or if indeed this is the best way to do it. Thanks for advice in advance!
I think you are doing it the wrong way , If you have similar pages for different role then I think you are accessing same model for handling different roles.
If this is the case then you should use Gem like Cancan (authorization library for Ruby on Rails).
To address your second concern, I don't think this would be the best approach. Try this - move your logic away from multiple controllers, and keep the logic in the ApplicationController. I do not think that you should separate the different roles to controllers. Instead, look to your User model and put in a method that checks the privilege level for the different users. You could create methods in your User model that you could call in your controllers to see if the user is allowed access to the action. before_action would be recommended here.
If you decide to keep multiple controllers, I recommend a gem like Authority. Makes it much easier to keep track of different privileges, even across different controllers - I think routing is addressed as well: https://github.com/nathanl/authority
I have an admin controller and view to manage admin tasks. Many of those tasks are very similar to tasks conducted in my two main model-backed controllers, Users and Materials. I'm trying to dry up my code so I want to put it somewhere, but where?
For example:
As an admin I can delete a Material from my admin view but so can a User from their material view. I have almost identical code for this in both the admin and material controllers with the only exception that the redirect goes to a different place.
The Rails4 way is to use Concerns, even though there is some discussion going on about it. Still, I like this approach, even though most of the material you find will be more about models than about controllers.
A simple example
If you are on Rails 3 (as your tag implies), just add a concerns-folder into your controllers-folder and add it to your autoload-path:
#config/application.rb
config.autoload_paths += %W(#{config.root}/app/controllers/concerns)
For instance, I have something like this in app/controllers/concerns/can_can_sanitizer.rb
module CanCanSanitizer
extend ActiveSupport::Concern
included do
before_filter do
resource = controller_path.singularize.gsub('/', '_').to_sym
method = "#{resource}_params"
params[resource] &&= send(method) if respond_to?(method, true)
end
end
end
I include this into my application_controller just like any other module:
include CanCanSanitizer
Admittedly, not the best use-case, but it should give you a headstart.
If the AdminsController is inherited from UsersController, you can put such methods in UsersController, judging the difference from method arguments or controller name or code before super.
If Admin and User has no inheritance, you can create a separate module and get both Admin and User to include it.
i have an app with nested resources. my routes are:
resources :teams do
resources :blogs
end
in my blogs controller, im using a different layout, by adding
layout "teamlayout"
to the controller.
Both layouts, the application.html.erb and the teamlayout.html.erb have included a login form itself. which i made working by this: https://github.com/plataformatec/devise/wiki/How-To:-Display-a-custom-sign_in-form-anywhere-in-your-app
now my question. when a user logs in, i want him redirected to the page from where he logs in.
You have a couple options:
Include a hidden field value in the login form that describes the source of the login (e.g. hidden_field_tag(:login_source, "team")) and define your own logic for SessionsController#create that uses the hidden field value to determine the location for response_with.
Or, you could keep track of the user's location by using a before_filter in the controllers with login forms by doing something like
def store_location
session['saved_location'] = request.request_uri
end
Then, you can override the after_sign_in_path_for(resource) method in your application controller to use the session saved_location value to determine where to redirect.
The second option seems a little less invasive to the Devise infrastructure to me, but is a little less flexible.
So I'm wondering what the best way to do static pages in Rails is, or rather Rails 3. I've always been a little confused about this, like should I create a controller or not?
There are a variety of approaches you can take (they're not all good approaches).
1 - public directory
If it's truly static, you can put it in the public directory. Things in the public directory will be served immediately, without going through the Rails stack.
Advantages:
Because it doesn't have to waste time going through the Rails stack, the client will receive a faster response.
Disadvantages:
You won't be able to use your sites layout. Or view helpers like link_to.
Instead of your views being in one place (app/views), now they're in two places (app/views and public). This can be confusing.
Thoughts: I feel pretty strongly that the disadvantages outweigh the advantages here. If you're looking to make a minor improvement in speed at the expense of readability and programmer happiness, why use Rails in the first place?
2 - Place in app/views and render directly from the Router
It is possible to render views from the router. However, it definitely isn't The Rails Way.
From the official RailsGuide on routing:
1 The Purpose of the Rails Router
The Rails router recognizes URLs and dispatches them to a controller's action.
Architecturally, there isn't anything inherently wrong with having a router map directly to a view. Many other frameworks do just that. However, Rails does not do that, and deviating from an established convention is likely to confuse other developers.
like should I create a controller or not?
Unless you want to take one of the approaches mentioned above - yes, you should create a controller.
The question then becomes what to name the controller. This answer outlines some options. I'll list them here with some thoughts. I'll also add three other options.
3 - Use ApplicationController
# routes.rb
get "/about" to: "application#about"
# application_controller.rb
class ApplicationController < ActionController::Base
def about
render "/about"
end
end
# app/views/about.html.erb
The advantage here is that you don't introduce overhead/bloat by creating a new controller and folder. The disadvantage is that it's not The Rails Way. Every controller you create inherits from ApplicationController. ApplicationController is typically used to house functionality that you want to share between all other controllers. See this example from the Action Controller Overview Rails Guide:
class ApplicationController < ActionController::Base
before_action :require_login
private
def require_login
unless logged_in?
flash[:error] = "You must be logged in to access this section"
redirect_to new_login_url # halts request cycle
end
end
end
4 - StaticController or StaticPagesController
Michael Hartl's popular Ruby on Rails Tutorial uses a StaticPagesController. I agree with the source I got this from in that I don't like this approach because the pages often aren't actually static.
Also, there is a possibility of confusion - why did we put other static views in separate controllers? Shouldn't static views be rendered from the StaticPagesController? I don't think the possibility of confusion is too high, but still wanted to note it.
Also note Hartl's footnote:
Our method for making static pages is probably the simplest, but it’s not the only way. The optimal method really depends on your needs; if you expect a large number of static pages, using a Static Pages controller can get quite cumbersome, but in our sample app we’ll only need a few. If you do need a lot of static pages, take a look at the high_voltage gem. ↑
5 - PagesController
The official Ruby on Rails routing guide uses PagesController. I think this approach is fine, but it isn't descriptive at all. Everything is a page. What distinguishes these pages from the other pages?
6 - UncategorizedPagesController
I would call the controller UncategorizedPagesController, because that's exactly what they are - uncategorized pages. Yes, it's a little more cumbersome to type and read. I prefer the advantage of clarity over conciseness, but I could understand the choice to be more concise and go with PagesController, or something else.
7 - High Voltage gem
With High Voltage, you don't have to do the tedious work of writing out routes and empty controller actions:
# routes.rb
root 'pages#home'
get '/about', to: 'pages#about'
get '/contact', to: 'pages#contact'
get '/help', to: 'pages#help'
get '/terms-of-service', to: 'pages#terms_of_service'
get '/landing-page', to: 'pages#landing_page'
...
# pages_controller.rb
def PagesController < ApplicationController
def home
end
def about
end
def contact
end
def help
end
def terms_of_service
end
def landing_page
end
...
end
You just add your pages to app/views/pages and link to them: <%= link_to 'About', page_path('about') %>.
It depends on if they're truly static. You can always add pages to the public/ directory of your app, and they'll work just fine. They don't even fire up rails or touch the routing engine.
However, most pages on the site, including static, still need to use the site's layout. You don't want to have to update the layout in dozens of pages separately. In this case, you can create a "catchall" controller. Here's an example:
rails g controller site home about_us location
Then you can put the page-specific content in app/views/site/home.html.erb, for instance.
UPDATE: You can go a step further and cache those pages with a call to caches_page near the top of your controller:
class SiteController < ApplicationController
caches_page :home, :about_us, :location
end
Just be aware that if you have dynamic page elements, like a list of links that changes based on whether the user is logged in, page caching isn't going to work for you. But this info should get you pointed in the right direction.
If they're 100% static, just shove them into public.
For example, when you spin up a new rails project, there's an index.html in your public.
If you want nicer routing, then yeah, creating a controller probably wouldn't be a bad idea.
I prefer to create a controller. Good tutorial and explanation on static pages:
http://railstutorial.org/chapters/static-pages#top