I have two models:
Student
Classroom
Both of them have an action that does the same exact thing: it shows a report of daily activity. That is:
/students/1
/classrooms/1
Grabs activity for the model in question and displays it on the page.
In an attempt to dry this up, I created a ReportsController which extracts all the common logic of building a report.
If I leave the routes like this:
/students/1/report
/classrooms/1/report
Then I can have the ReportsController#show action look for params for :student_id or :classroom_id to determine which model type it is dealing with (for purposes of querying the database and rendering the correct view).
But I would prefer the URLs to be cleaner, so I also changed my routes.rb file to pass the show action for these models to the reports#show controller action:
resources :students, :classrooms do
member do
get :show, to: 'reports#show'
end
end
This works, but I can no longer depend on params to identify which model to work with and which view to render.
Question: should I parse request.fullpath for the model? Or is there a better way to make a shared controller understand which model it is working with?
Routing both show methods to the same controller method for code reuse is somewhat like banging a nail in with a dumptruck.
Even if you can find the resource by looking at the request url you would start splitting the ResortsController into a bunch of ifs and switches even before you got off the ground.
One solution is to add the common action in a module:
module Reporting
extend ActiveSupport::Concern
def show
# the Student or Classroom should be available as #resource
render 'reports/show'
end
included do
before_action :find_resource, only: [:show]
end
private
def find_resource
model = self.try(:resource_class) || guess_resource_class
#resource = model.find(params[:id])
end
# This guesses the name of the resource based on the controller name.
def guess_resource_class
self.class.name[0..-11].singularize.constantize
end
end
class StudentController < ApplicationController
include Reporting
end
# Example where resource name cannot be deduced from controller
class PupilController < ApplicationController
include Reporting
private
def resource_class
Student
end
end
self.class.name[0..-11].singularize.constantize is basically how Rails uses convention over configuration to load a User automatically in your UsersController even without any code.
But the most important key to DRY controllers is to keep your controllers skinny. Most functionality can either be moved into the model layer or delegated out to service objects.
I would put the common logic in the Event Model:
#Event Model
class Event < ...
def self.your_event_method
#self here will be either student.events or classroom.events
#depending on which controller called it
end
end
class StudentsController < ...
...
def show
student = Student.find(params[:id])
student.events.your_event_method
end
end
class ClassroomsController < ...
...
def show
classroom = Classroom(params[:id])
classroom.events.your_event_method
end
end
Related
Example code:
#model
class Profile < AR:Base
has_many :projects
end
#controller
class ProfilesController < AC
def show
#user = Profile.find(params[:id])
end
end
#view
#profile.projects.each do |project|
= render something
end
Any user can view any profile, but projects should be filtered by visibility (like public/private projects).
I'm concerning to add one more ivar because it violates Sandi Metz's rule
Controllers can instantiate only one object. Therefore, views can only
know about one instance variable and views should only send messages
to that object (#object.collaborator.value is not allowed).
The only way I see it now is to introduce another class (facade) to do this things, like:
class ProfilePresenter
def initialize(profile, current_user)
#profile = profile
#current_user
end
def visible_profiles
ProjectPolicy::Scope.new(current_user, profile.projects).resolve
end
end
Am I missing something?
How would one achieve it (resolving association scopes) using Pundit?
In case we will need pagination for projects within profile view - what approach to choose?
I have a question regarding the reuse of code among controller actions. I think it is a fairly standard situation, so I am interested in what's the best practice in Rails.
Let's say I have a films resource with a corresponding FilmsController, which has a nested resource comments served by CommentsController. The nested resource can be rendered on its own using its index and show actions. However, it should also be possible to render the comments embedded in the corresponding film page.
Now, the question goes, what is the best way to reuse the code from CommentsController within FilmsController.show?
1) Force the CommentsController.index to render to a string and then pass it in a variable to the film view?
Or 2) call the CommentsController.index directly in the film view as a kind of "partial", executing the database queries from there?
Or 3) create a separate method in CommentsController responsible for the database handling, call it from both CommentsController.index and FilmsController.show, and use the corresponding view in both the places, too?
To me the options 1) and 2) seem a bit messy, while 3) is not modular and involves some repeating of code. Is there any better way to accomplish this?
Thanks a lot!
Now, the question goes, what is the best way to reuse the code from CommentsController within FilmsController.show?
You could move the shared controller logic into a inside your application controller (or a lib and require it appropriately), a la:
class ApplicationController < ActionController::Base
def foo
#foo = "foo"
end
end
Comments Controller:
class CommentsController < ApplicationController
before_filter :foo, :only => [:index]
def index
end
end
Films Controller:
class FilmsController < ApplicationController
before_filter :foo, :only => [:show]
def show
end
end
For repeated view logic you can move that to a common folder, say your_app/app/views/shared/_foo.html.erb and render that appropriately.
Another option is to place the relevant code into an external module:
lib/mymodule.rb
module MyModule
def foo
end
end
And then you can include the module inside your controller or anywhere you want access to your foo method.
class CommentsController < ApplicationController
include MyModule
def index
foo()
end
end
Rails form validation is designed to go in the model most easily. But I need to make sure the current user has the required privileges to submit a post and the current_user variable is only accessible in the controller and view.
I found this answer in a similar question:
You could define a :user_gold virtual attribute for Book, set it in the controller where you have access to current_user and then incorporate that into your Book validation.`
How can I set this up with my post and user controller so that the current_user variable is accessible in the model?
Solution:
This whole thing is wrong from an application design perspective as #Deefour's answer pointed out. I changed it so my view doesn't render the form unless the condition is true.
The "similar question" is saying you can do something like this
class YourModel < ActiveRecord::Base
attr_accessor :current_user
# ...
end
and then in your controller action you can do something like
#your_model = YourModel.find(params[:id])
#your_model.current_user = current_user
#your_model.assign_attributes(params[:your_model])
if #your_model.valid?
# ...
You can then use self.current_user within YourModel's validation methods.
Note I don't think this is what you should be doing though, as I don't consider this "validation" as much as "authorization". An unauthorized user shouldn't even be able to get the part of your action where such an update to a YourModel instance could be saved.
As for doing the authorization with Pundit as requested, you'd have a file in app/policies/your_model.rb
class YourModelPolicy < Struct.new(:user, :your_model)
def update?
user.some_privilege == true # change this to suit your needs, checking the "required privileges" you mention
end
end
Include Pundit in your ApplicationController
class ApplicationController < ActionController::Base
include Pundit
# ...
end
Then, in your controller action you can do simply
def update
#your_model = YourModel.find(params[:id])
authorize #your_model
# ...
The authorize method will call YourModelPolicy's update? method (it calls the method matching your action + ? by default) and if a falsy value is returned a 403 error will result.
Authorization shouldn't be done in models. Models have already many responsibilities don't you think?
That's a controller thing, and actually you can have the logic in other place using some gem like cancan and in your controller you would do something like:
authorize! :create, Post
You can define a "virtual attribute" in your model like this:
class Book < ActiveRecord::Base
attr_accessor :current_user
end
Its value can be set directly in your controller like this:
class BooksController < ApplicationController
def create
book = Book.new
book.current_user = current_user
book.save!
end
end
And inside your model's validation routine, you can access it like any other ActiveRecord field:
def validate_user_permission
errors[:current_user] = "user does not have permission" unless current_user.is_gold?
end
I can't remember if this is the case with ActiveRecord, but you might be able to set virtual attributes via the mass-assignment methods like create, update, and new in the controller:
def create
Book.create!(current_user: current_user)
end
In order to do that, you would probably have to add the following line to your model to enable mass-assignment of that virtual attribute:
attr_accessible :current_user
I agree with Ismael - this is normally done in the controller. It's not an attribute of the model, it's a permission issue and related to the controller business logic.
If you don't need all the power of a gem like CanCan, you can role your own.
class BooksController < ApplicationController
before_filter :gold_required, :only => :create
def create
book = Book.new
book.save!
end
# Can be application controller
private
def gold_required
return current_user && current_user.is_gold?
end
end
You may want to put the filter on the 'new' method as well.
class HouseBuyersController < ...
def my_method
# How could I get here the relevant model name, i.e. "HouseBuyer" ?
end
end
This will do it:
class HouseBuyersController < ApplicationController
def index
#model_name = controller_name.classify
end
end
This is often needed when abstracting controller actions:
class HouseBuyersController < ApplicationController
def index
# Equivalent of #house_buyers = HouseBuyer.find(:all)
objects = controller_name.classify.constantize.find(:all)
instance_variable_set("##{controller_name}", objects)
end
end
If your controller and model are in the same namespace, then what you want is
controller_path.classify
controller_path gives you the namespace; controller_name doesn't.
For example, if your controller is
Admin::RolesController
then:
controller_path.classify # "Admin::Role" # CORRECT
controller_name.classify # "Role" # INCORRECT
It's a bit of a hack, but if your model is named after your controller name then:
class HouseBuyersController < ApplicationController
def my_method
#model_name = self.class.name.sub("Controller", "").singularize
end
end
... would give you "HouseBuyer" in your #model_name instance variable.
Again, this makes a huge assumption that "HouseBuyersController" only deals with "HouseBuyer" models.
For namespaces working:
def resource_class
controller_path.classify.constantize
end
The accepted solution did not work for me as my controller and model was namespaced. Instead, I came up with the following method:
def controllers_model
(self.class.name.split('::')[0..-2] << controller_name.classify).join('::')
end
This is not possible if you are using the default MVC, which your code doesn't seem to follow. Your controller seems to be a model but maybe you just got a type there. Anyway, controllers and models are fundamentally separated in Rails MVC so controllers cannot know which model they are associated with.
For example you could have a model named post. This can have a controller posts_controller or could have a controller like articles_controller. Rails only knows about models when you def the actual code in the controller such as
def index
#posts = Post.all
#posts = Article.all
end
In rails standard controllers there is no way to know what the model is.
In my Ruby on Rails app, I've got:
class AdminController < ApplicationController
def create
if request.post? and params[:role_data]
parse_role_data(params[:role_data])
end
end
end
and also
module AdminHelper
def parse_role_data(roledata)
...
end
end
Yet I get an error saying parse_role_data is not defined. What am I doing wrong?
Helpers are mostly used for complex output-related tasks, like making a HTML table for calendar out of a list of dates. Anything related to the business rules like parsing a file should go in the associated model, a possible example below:
class Admin < ActiveRecord::Base
def self.parse_role_data(roledata)
...
end
end
#Call in your controller like this
Admin.parse_role_data(roledata)
Also look into using (RESTful routes or the :conditions option)[http://api.rubyonrails.org/classes/ActionController/Routing.html] when making routes, instead of checking for request.post? in your controller.
Shouldn't you be accessing the parse_role_data through the AdminHelper?
Update 1: check this
http://www.johnyerhot.com/2008/01/10/rails-using-helpers-in-you-controller/
From the looks of if you're trying to create a UI for adding roles to users. I'm going to assume you have a UsersController already, so I would suggest adding a Role model and a RolesController. In your routes.rb you'd do something like:
map.resources :users do |u|
u.resources :roles
end
This will allow you to have a route like:
/users/3/roles
In your RolesController you'd do something like:
def create
#user = User.find_by_username(params[:user_id])
#role = #user.roles.build(params[:role])
if #role.valid?
#role.save!
redirect_to #user
else
render :action => 'new'
end
end
This will take the role params data from the form displayed in the new action and create a new role model for this user. Hopefully this is a good starting point for you.