I am making a Rails 4 app and I have a BookingsController. There are two types of user for my app - admins and guests and both need to be able to view a booking - however, the view is very different depending on the type of user that is viewing the booking.
At the moment I am using a standard RESTful approach for the admins and my own actions in my GuestController for when the guests view bookings.
Is there a better approach to this problem? - and by better I mean is there a way to make the guests RESTful too?
Basically as Leszek already suggested go with namespaces. If the logic for both cases in controller is the same you can extract it in two ways:
Leave BookingsController as you have now and use inheritance - let your scoped controllers inherit from this non-namespaced controller like this:
# controllers/admin/bookings_controller.rb
class Admin::BookingsController < BookingsController
end
# controllers/guest/bookings_controller.rb
class Guest::BookingsController < BookingsController
end
Second option would me extract all methods to a module (concern)
# concerns/controllers/bookings.rb
module Controllers::Bookings
def show
...
end
end
# controllers/admin/bookings_controller.rb
class Admin::BookingsController < ApplicationController
include Controllers::Bookings
end
# controllers/guest/bookings_controller.rb
class Guest::BookingsController < ApplicationController
include Controllers::Bookings
end
Both ways allow You to override methods that will have different logic. Some people like the first approach, others prefer second. One thing to consider though - Ruby is a single inheritance language, so if You are already subclassing these controllers from some common parent controller then probably going with modules approach will be more flexible.
Related
I have a basic model called Page and have many STI models based on Page such as
Drawing
Article
Story
etc...
I have separate controller and view for each of these STI models because I needed to customized the view layer based on the model type and have different logic for controller and hence separate controllers. However, I need the author field of all models to be set to current user. How do I do this in a single place?
For example, if I use before_action on Page controller and set the author, it affects the #page instance variable whereas my DrawingsController is using #drawing so it wont save the author of my #drawing unless I repeat the same code in DrawingsController.
Edit:
My controller hierarchy is
DrawingsController < PagesController
PagesController < ApplicationController
Both PagesController and DrawingsController have all the 7 restful actions. However, the actions on PagesController doesn't serve any purpose as I dont want any of my users to create Pages. I only want them to create the inherited STI classes like Drawings.
you could do this using some convention and meta programming in your controller hierarchy:
def add_author
model = instance_variable_get(:"##{controller_name.singularize}")
model.author = current_user
end
I'll be honest, I make no guarantees about the "best-practice"-ness of this answer, but I'll propose it anyways in case it helps to some degree. Also note that after rethinking the problem, I realized my first suggested solution in the comments was wrong, and the second is also not quite right either. So I'm posting a modified version of the second suggestion only:
Short answer: Let the PagesController handle most of the work, and delegate only to the subcontroller for model-specific things if needed. As phoet said, you can use a bit of meta programming (in a different way) to accomplish this.
class PagesController < ApplicationController
# pages controller stuff here
def create
#page = controller_name.classify.constantize.new(params[:page_params]) # I love Rails, don't you?
#page.author = current_user
handle_additional_create_actions
# For completeness of this example...
if #page.save
# render / redirect on success
else
# render errors
end
end
protected
# This method should be overwritten by sub controllers if needed
# Also, name this whatever you like, this is merely a verbose example for illustration purposes
def handle_additional_create_actions
# in the pages controller, this method does nothing
end
end
And, if there are additional things that need to be done by the model-specific controller:
class DrawingsController < PagesController
# drawing controller stuff here
protected
def handle_additional_create_actions
#page.some_other_field = some_other_data
end
end
A quick note: Note that in my suggestion, you're eliminating the model-specific variable names, meaning we don't have an #drawing and an #article, etc, anymore. Your models are all, essentially, types of Page objects, and so we're going to call it by its general name as a convention. That way, when you ask the DrawingsController to do something specific for the Drawing class, it knows that instance can be accessed via our generically named #page object.
So ultimately, the PagesController does the heavy lifting, regardless of which concrete model type you're dealing with. That way, only general page stuff is found in the pages controller, and drawing, article or story-specific stuff is found in their respective concrete controllers.
I know this is 1001st question about global objects, but I think my situation is slightly different.
I'm working on ecommerce solution, which provides few different shops within a single rails application.
There is a class Shop which provides shop-specific logic and options. For example:
#shop.tax should be accessible in models. Tax can differ depend on shop. eg 9%, 18%.
#shop.name and #shop.layout should be accessible in controllers and views.
#shop.email.general for mailers.
I need to be able to create an instance of Shop in application controller and somehow pass it to the all application parts.
# controllers/application_controller.rb
class ApplicationController < ActionController::Base
before_filter :set_shop
protected
def set_shop
requested_shop = if request.domain.match(/^.*shop1\.com$/)
:shop_1
elsif request.domain.match(/^.*shop2\.com$/)
:shop_2
end
#shop = Shop.new(requested_shop)
end
end
I know that request-based logic should not be used in models, but I really need shop options there. In tests I could mock this "global" object like that Shop.new(:test_shop) in spec_helper.
Is global variable my only choice? I've never used them.
I tried to use Settingslogic gem, but it defines attr_accessors for shop-specific options, and they persist between requests, which is not what I need.
One way of doing this would be something like
class Shop
def self.current=(shop)
Thread.current[:current_shop] = shop
end
def self.current
Thread.current[:current_shop]
end
end
Which allows you to maintain a separate current shop for each request.
The alternative is to pass the current shop around. It may seem tedious at first but can ultimately be simpler to reason about than global or pseudo global behaviour
This is going to sound strange, but hear me out...I need to be able to make the equivalent of a POST request to one of my other controllers. The SimpleController is basically a simplified version of a more verbose controller. How can I do this appropriately?
class VerboseController < ApplicationController
def create
# lots of required params
end
end
class SimpleController < ApplicationController
def create
# prepare the params required for VerboseController.create
# now call the VerboseController.create with the new params
end
end
Maybe I am over-thinking this, but I don't know how to do this.
Inter-controller communication in a Rails app (or any web app following the same model-adapter-view pattern for that matter) is something you should actively avoid. When you are tempted to do so consider it a sign that you are fighting the patterns and framework your app is built on and that you are relying on logic has been implemented at the wrong layer of your application.
As #ismaelga suggested in a comment; both controllers should invoke some common component to handle this shared behavior and keep your controllers "skinny". In Rails that's often a method on a model object, especially for the sort of creation behavior you seem to be worried about in this case.
You shouldn't be doing this. Are you creating a model? Then having two class methods on the model would be much better. It also separates the code much better. Then you can use the methods not only in controllers but also background jobs (etc.) in the future.
For example if you're creating a Person:
class VerboseController < ApplicationController
def create
Person.verbose_create(params)
end
end
class SimpleController < ApplicationController
def create
Person.simple_create(params)
end
end
Then in the Person-model you could go like this:
class Person
def self.verbose_create(options)
# ... do the creating stuff here
end
def self.simple_create(options)
# Prepare the options as you were trying to do in the controller...
prepared_options = options.merge(some: "option")
# ... and pass them to the verbose_create method
verbose_create(prepared_options)
end
end
I hope this can help a little. :-)
I have an app that has users whose profiles are accessible via site.com/username. When choosing a username, I make an AJAX call to a method in my UsersController to make sure the username is available (and check on the back end as well when submitted). I now want to add groups that will also be accessible through site.com/groupname. Since group and user names cannot collide, whatever controller method that responds to the AJAX call will need to check both so the check_username_available and check_groupname_available methods will do the exact same thing. What's the best practice / Rails way to handle this since I don't want to replicate code in both UsersController and GroupsController?
Having a method for each controller seems a bit redundant, even if the functionality is pulled out to a helper, since there will still be two routes that do the same thing. Having a separate controller solves the problem too but not sure this is good Rails practice.
code that is reused can be shared via a module
class UsersController < ActionController::Base
include NameUniqueness
end
class GroupsController < ActionController::Base
include NameUniqueness
end
module NameUniqueness
protected
def check_name
# implementation here
end
end
both controllers will now have access the check_name instance method.
DanPickett's answer is great.
Another choice is to make a class method in the user model and just call it from each controller. Since this name checking seems like a job for the model, that's what I would do.
class User
def self.check(stuff) ...
In my previous learning projects I always used a single controller, but now I wonder if that is good practice or even always possible.
In all RESTful Rails tutorials the controllers have a show, an edit and an index view. If an authorized user is logged on, the edit view becomes available and the index view shows additional data manipulation controls, like a delete button or a link to the edit view.
Now I have a Rails application which falls exactly into this pattern, but the index view is not reusable:
The normal user sees a flashy index page with lots of pictures, complex layout, no Javascript requirement, ...
The Admin user index has a completly different minimalistic design, jQuery table and lots of additional data, ...
Now I'm not sure how to handle this case. I can think of the following:
Single controller, single view: The view is split into two large blocks/partials using an if statement.
Single controller, two views: index and index_admin.
Two different controllers: BookController and BookAdminController
None of these solutions seems perfect, but for now I'm inclined to use the 3rd option.
What's the preferred way to do this?
I asked myself this question almost every time when I get a new project. I usually choose one of the two solutions:
1). Single Controller, Single View
I almost never choose this solution now a days unless the project is really simple, and only one or two types of users. If you get multiple user types it is better to use solution # 2. Although this solution may be appealing because you think you save yourself some time by writing less code, but in the end, your controller and view will grow in complexity. Not to mention all the edge cases you have to consider. This usually means bugs.
My company once had to rescue a failed project, it had 3 user types. (admin, business, and member). They used solution #1. The code was in a horrible condition, ( and that's why we were asked to rescue this project) We were jokingly saying it is not MVC, it was MMM. (Model-Model-Model) This is because business logic was not properly extracted and put into models, but spread in controllers and views as well.
2). Multiple Controller, Multiple Views
I use this solution more and more these days. I usually namespace the controllers with user types. For example:
In "app/controllers"
class BookController < ApplicationController
end
and in "app/controllers/admin"
class Admin::BookController < Admin::BaseController
end
I only need to consider regular users when I fill in BookController, and only need to consider admin users when I fill in Admin::BookController
I'm not sure if there are better ways, but this is what I learned from a dozen projects I've done so far...
What I do in such a situation changed quite a bit lately. The current approach is as follows:
I separate controllers based on access requirements. This gives me a clear
mental model and a very easy way to check for access control (and test it).
I even go as far as to separate 'your own access' to models into separate
controller. I also usually keep the controller's name just put it into a
separate namespace.
This approach is also makes it very easy to use standard restuful controller
implementations like InheritedResources.
Note that you can reuse many of the views if same functionality is required for
different kinds of access.
So I'd have something like this:
### lets start with routes
# this is basically guest access level. you can only list it and see details
map.resources :books, :only => [:index, :show]
namespace :my do |my|
# this will require at least login.
# index and show will be basically same as at the guest level. we can reuse the views
my.resources :books
end
namespace :admin do |admin|
# this will require admin login
admin.resources :books
end
# now the controllers
# this might be just enough of a controller code :). the rest goes into model.
class BooksController < InheritedResources::Base
actions :index, :show
end
module My
class BooksController < InheritedResources::Base
before_filter :require_user
protected
def begin_of_association_chain
# this will force resources to be found on current_user.books.
# so if you go to /my/books/123 and book 123 is not your book you will get 404
current_user
end
end
end
module Admin
class BooksController < InheritedResources::Base
before_filter :require_admin
# this controller is essentially unrestricted. you can get/edit/update any book
# and you can have separate view template for index too
end
end
## Views
# well, this is the part that I like the least in this approach, but
# I think the good outweight the bad.
# I see 2 ways to reuse the views w/o writing lots of ugly customization code for the InheritedResources
# 1) render 'parent' views inside templates. i.e. like:
# my/books/index.html.haml:
!= render :file => "/books/index"
# or just link the templates on the filesystem level with symlinks.
# (or if all of them are the same you can link the directory)
Use two current if there are two modules 1] Admin 2] User
Say
class BookUserController < ApplicationController
#Here layout is used will be of layouts/application.html.erb
#Here all the methods which user can will be present
end
class BookAdminController < ApplicationController
layout 'admin' #here we set the layout is layouts/admin.html.erb
end
IF Only one page you want to show admin you can use single controller and two methods
class BookUserController < ApplicationController
layout 'admin', :only=>['index_admin']
def index_admin
end
def index
end
end
OR
class BookUserController < ApplicationController
def index_admin
render :action=>'index_admin', :layout => false
end
def index
end
end
When I need a clearly separated administration area, I tend to go for a solution with two controllers for one resource. An Admin::BooksController in the admin/ sub directory for the admin interface with all actions and a public BooksController that only has index and show methods depending on needs.