How to extend a controller method from a Rails Engine without having to duplicate the whole thing?
Trying to extend https://github.com/radar/forem/blob/rails4/app/controllers/forem/forums_controller.rb -- app/decorators/controllers/forem/forums_controller_decorator.rb:
Ideal
Forem::ForumsController.class_eval do
def show
# A simple `include` here or something?
# New code goes here...
end
end
Current
Forem::ForumsController.class_eval do
def show
# Repeat ALL the code from:
# https://github.com/radar/forem/blob/rails4/app/controllers/forem/forums_controller.rb
authorize! :show, #forum
register_view
#topics = if forem_admin_or_moderator?(#forum)
#forum.topics
else
#forum.topics.visible.approved_or_pending_review_for(forem_user)
end
#topics = #topics.by_pinned_or_most_recent_post
# Kaminari allows to configure the method and param used
#topics = #topics.send(pagination_method, params[pagination_param]).per(Forem.per_page)
respond_to do |format|
format.html
format.atom { render :layout => false }
end
# New code goes here...
end
end
We use this gem for multiple applications and engines to do exactly what you want:
https://github.com/EPI-USE-Labs/activesupport-decorators
I could extend a controller method from a Rails Engine without having to duplicate code using alias_method
module ValueSets
SetsController.class_eval do
def new_with_authorize
new_without_authorize
authorize #value_set
end
alias_method :new_without_authorize, :new
alias_method :new, :new_with_authorize
end
end
Related
Is there a way to use the same definition for 2 actions?
Something like this?
def index2, index5
#workgroups = Workgroup.all
if params[:workgroup]
#workgroup = Workgroup.find(params[:workgroup])
end
respond_to do |format|
format.html # index.html.erb
format.json { render json: #workgroups }
end
end
You can route the 2 urls to the same action.
On config/routes.rb do:
get 'controller/index5', to: 'controller#oneindex'
get 'controller/index2', to: 'controller#oneindex'
Three solutions
1 Make Common method
I would prefer this approach because in case if you want to change something...
def index5
common_method_name
end
def index2
common_method_name
end
private
def common_method_name
# common code
end
2 Alias
def index5
# your code
end
alias_method :index2, :index5
3 routes
You can route the 2 urls to the same action in config/routes.rb.(source Murifox's answer)
get 'controller/index5', to: 'controller#oneindex'
get 'controller/index2', to: 'controller#oneindex
Using RABL in Rails 3.2.x, given the following controller action:
respond_to :html, :json
def create
#foo = Foo.create(params[:foo])
respond_with #foo
end
Assuming the validation fails, how do you get respond_with to use a RABL template instead of the standard JSON hash of errors -- IE. I would like other model attributes besides the validation error message sent back along with the request.
Suggestions?
I found this one out the hard way. You should create a custom responder for your application controller, or at least your individual response. See Three reasons to love ActionController::Responder for more details.
My solution:
# app/responders/api_responder.rb
class ApiResponder < ActionController::Responder
def to_format
case
when has_errors?
controller.response.status = :unprocessable_entity
when post?
controller.response.status = :created
end
default_render
rescue ActionView::MissingTemplate => e
api_behavior(e)
end
end
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
#...
self.responder = ApiResponder
#...
end
You could also use respond_with #foo, responder: ApiResponder instead.
Thanks to haxney for sending me in the right direction.
I guess, you need to remove the respond_to call at the top of the controller and remove the respond_with call within the action to get rabl render your rabl template.
Just add a respond_to block at the end of each action where you don't need RABL.
I seem to have an authorization hiccup in my Ruby on Rails app. I have been using the following method in my application controller and it has been working beautifully.
def require_owner
obj = instance_variable_get("##{controller_name.singularize.camelize.underscore}") # LineItem becomes #line_item
return true if current_user_is_owner?(obj)
render_error_message("You must be the #{controller_name.singularize.camelize} owner to access this page", root_url)
return false
end
I then filter in the specific controllers by:
before_filter :require_owner, :only => [:destroy, :update, :edit]
I recently created a new controller which has a bit of a different naming convention that seems to be causing a problem. Normally my controllers read messages_controller or posts_controller. In this specific case I named the resource box_wod which generated box_wods_controller.
This is the only controller that seems to be having a problem with this filter so I bet I can tell it is in the naming of it and therefore the application_controller method is not recognizing the owner of the record.
I am not getting an error message but the application is not letting me edit, update or destroy a record because I am not the BoxWod owner. My routes are correct as are my associations and the correct information is getting passed to the box_wod table.
Is there a way to rewrite the application_controller method to recognize the additional underscore in the box_wod resource? Or is this even my problem?
UPDATE:
Here are the three methods in the BoxWodsController:
def edit
#workout_count = Workout.count
#box_wod = BoxWod.find(params[:id])
end
def update
#box_wod = BoxWod.find(params[:id])
respond_to do |format|
if #box_wod.update_attributes(params[:box_wod])
flash[:notice] = 'BoxWod was successfully updated.'
format.html { redirect_to(#box_wod) }
format.xml { head :ok }
else
format.html { render :action => "edit" }
format.xml { render :xml => #box_wod.errors, :status => :unprocessable_entity }
end
end
end
def destroy
#box_wod = BoxWod.find(params[:id])
#box_wod.destroy
respond_to do |format|
format.html { redirect_to(box_wods_url) }
format.js
end
end
In situations like this, I like to create a controller method that I can override when necessary. For example:
# application_controller.rb
class ApplicationController
def require_owner
obj = instance_variable_get("##{resource_instance_variable_name}")
# Do your authorization stuff
end
private
def resource_instance_variable_name
controller_name.singularize.camelize.underscore
end
end
# box_wods_controller.rb
class BoxWodsController
private
def resource_instance_variable_name
'box_wod' # Or whatever your instance variable is called
end
end
Lastly, please post your BoxWodsController code so we can better diagnose the problem.
It would seem that the #box_wod instance variable is not created until the require_owner method is invoked so current_user_is_owner? is checking a nil value, resulting in it always returning false. Perhaps you need another before_filter to populate the instance variable before require_owner is invoked.
I have the following:
def create
#permission = #project.permissions.create(params[:permission])
respond_to do |format|
if #permission.save
format.js
else
format.js { render :js => #permission.errors }
end
end
end
I want to add in a Mailer, to let the user know they have been added to a project, the issue is, if I pu that before the respond_to, the record hasn't been saved yet so it's possible that something could go wrong but the user would still get an email.
UserMailer.xxxxxxxxx_notification(objecthere).deliver
And I'm guessing I can't put a mailer inside the respond_to block. Suggestions?
This is what observers are used for.
create app/models/permission_observer.rb
class PermissionObserver < ActiveRecord::Observer
def after_create(permission)
# put your mailer code here
end
end
in config/application.rb add the observer
config.active_record.observers = :permission_observer
You can read more about observers here.
Also, you should be using #project.permissions.new instead of create. create saves the model immediately, making your #permission.save call redundant.
Once you have this in place, you should look into making your mailer code asynchronous so it doesn't hold up web requests. Here's an example using delayed_job.
Or you can edit your code to:
def create
#permission = #project.permissions.build(params[:permission])
if #permission.save
UserMailer.xxxxxxxxx_notification(objecthere).deliver
respond_to do |format|
format.js
end
else
respond_to do |format|
format.js { render :js => #permission.errors }
end
end
end
Or you can put a callback in your Permission model:
class Permission
after_create :send_mail
def send_mail
UserMailer.xxxxxxxxx_notification(self).deliver
end
end
It's very simple, I want to handle a normal [show] request with a call to DataMapper like I did in Merb.
With ActiveRecord I could have done this:
class PostsController
def show
#post = Post.get(params[:id])
#comments = #post.comments unless #post.nil?
end
end
and it handles the 404 by catching the resource's exceptions.
DataMapper instead doesn't do this automatically so right now I'm solving it with this solution:
[moved in the answers]
It is possible to tell the controller to halt inside the not_found function?
I like to use exception throwing, and then use ActionController's rescue_from.
Example:
class ApplicationController < ActionController::Base
rescue_from DataMapper::ObjectNotFoundError, :with => :not_found
def not_found
render file => "public/404.html", status => 404, layout => false
end
end
class PostsController
def show
#post = Post.get!(params[:id]) # This will throw an DataMapper::ObjectNotFoundError if it can't be found
#comments = #post.comments
end
end
Done 'the old Merb way':
class ApplicationController
def not_found
render file: "public/404.html", status: 404, layout: false
end
end
class PostsController
def show
#post = Post.get(params[:id])
not_found; return false if #post.nil?
#comments = #post.comments
end
end
again: It is possible to tell the controller to halt inside the not_found function instead of explicitly calling 'return false' in the show action?
edit: thanx to Francois that found a better solution:
class PostsController
def show
#post = Post.get(params[:id])
return not_found if #post.nil?
#comments = #post.comments
end
end
As DM documentation says, you can use #get!