Given the following simplified situation (in reality, the scenario is from an ActiveAdmin backed app):
class ShapeController < ApplicationController
def update
(...)
redirect_to
end
end
class CircleController < ShapeController
def update
super
(...)
redirect_to
end
end
Calling CircleController#update will cause the famous "AbstractController::DoubleRenderError" because redirect_to is called twice.
Now, I can't prevent the first call of redirect_to by super, at least not without messing with ActiveAdmin's code. Is there another way to cancel the first redirect_to and overrule it with another one?
Thanks for your hints!
ActiveAdmin is using Inherited Resources to do perform the standard REST actions. The gem provided a way to overwrite the respond_to block. I've never try this before but this might be helpful in your case:
ActiveAdmin.register Circle do
# ...
controller do
def update
update! do |success, failure|
failure.html { redirect_to circle_url(#circle) }
end
end
end
# ...
end
Refer to the IR gem documentation for more options to overwrite the actions(under Overwriting actions section).
I would say it is not possible. The best solution would be to extract the action code in some protected controller method, and call it from the child controller:
class ShapeController < ApplicationController
def update
do_the_update
redirect_to
end
protected
def do_the_update
# your code
end
end
class CircleController < ShapeController
def update
do_the_update
redirect_to
end
end
Related
I'm trying to centralize authentification in pundit policies instead of having it in my controllers. It works well but I lost some flexibility in customizing redirection and flash message.
How could I transfer the information about which authentification didn't pass to the Pundit::NotAuthorizedError rescuing function ? One action can have 2 steps of authentification: 1. user.paid? 2. user.is_allowed_to_update? and I want custom message and redirection for each case.
The exception.query solution is not working cause it only allow to customize flash and redirection for each action and not within one action.
Below is a more detailed explanation of the situation
WITHOUT PUNDIT
Comment_Controller
def update
if user.didnt_pay?
flash[:message] = nice_message
redirect_to payment_page_path
elsif user.is_not_allowed_to_perform_action
flash[:message] = less_nice_message
redirect_to dashboard_path
end
end
And now
WITH PUNDIT
Comment_Controller
def update
authorize #comment
end
Comment_policy
def update?
user.paid? && user_is_allowed_to_perform_action
end
ApplicationController
rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
def user_not_authorized
flash[:message] = one_message_for_all_error # THIS IS WHAT I WANT TO CUSTOMIZE
redirect_to one_path_for_all_error # THIS IS WHAT I WANT TO CUSTOMIZE
end
A possibility for customizing this error messages it to set the expected message in the Policy and later getting it from the controller. How to do so?
The exception object you get as an argument in the controller
class CommentsController < ApplicationController
def user_not_authorized(exception)
end
end
comes with a policy attribute which links you to the offending policy. So, lets say that in your policy you want to set a particular message when some clause is not fulfilled:
class AnimalPolicy < ApplicationPolicy
attr_accessor :error_message
def new?
if !authorization_clause
#error_message = "Something terrible happened"
false
else
true
end
end
end
Hence, in your controller, you would have to set this error_message into your flash or wherever you want it to be:
class CommentsController < ApplicationController
def user_not_authorized(exception)
flash[:error] = exception.policy.try(:error_message) || "Default error message"
redirect_to root_path
end
end
It's a bit clumsy solution, but it worked for me
In my solution I propose two methods, one when the user has a good answer the other when the answer is not favorable ... pundit has a method (user_not_authorized) which allows to manage that one could duplicate and Adapt to your suggestions
def update
if user.didnt_pay?
authorize #comment
user_congratulation
elsif user.is_not_allowed_to_perform_action
user_not_authorized
end
end
in ApplicationController
past this rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
and after You'll create two private methods in your controller called
user_not_authorized and user_congratulation
private
def user_not_authorized
flash[:alert] = "less_nice_message"
redirect_to dashboard_path
end
def user_congratulation
flash[:alert] = "nice_message"
redirect_to payment_page_path
end
end
for more information visit this link https://github.com/elabs/pundit#rescuing-a-denied-authorization-in-rails
Although this post is old, I thought fit to answer because I was also in need of a good answer, which was not the case! I hope to have helped
how to make this code clean in rails?
profiles_controller.rb :
class ProfilesController < ApplicationController
before_action :find_profile, only: [:edit, :update]
def index
#profiles = Profile.all
end
def new
#profile = Profile.new
end
def create
profile, message = Profile.create_object(params["profile"], current_user)
flash[:notice] = message
redirect_to profile_url
end
def edit
end
def update
profile, message = #profile.update_object(params["profile"])
flash[:notice] = message
redirect_to profile_url
end
private
def find_profile
#profile = Profile.friendly.find(params["id"])
end
end
i look flash[:notice] and redirct_to profile_url is duplicate in my code, how to make the code to clean and dry?
How about moving the repetitive code to a separate method and call that method inside the actions.
def flash_redirect # you can come up with a better name
flash[:notice] = message
redirect_to profile_url
end
then in update action:
def update
profile, message = #profile.update_object(params["profile"])
flash_redirect
end
do the same thing for create action
UPDATE:
in case you are wondering about usingafter_action, you can't use it to redirect as the call-back is appended after the action runs out its course. see this answer
Take a look at Inherited Resources. It's based on the fact that many CRUD controllers in Rails have the exact same general structure. It does most of the work for you and is fully customisable in case things are done a little different in your controllers.
Using this gem, your code would look like this:
class ProfilesController < InheritedResources::Base
def create
redirect_to_profile(*Profile.create_object(params[:profile], current_user))
end
def update
redirect_to_profile(*#profile.update_object(params[:profile]))
end
private
def redirect_to_profile(profile, message)
redirect_to(profile_url, notice: message)
end
def resource
#profile ||= Profile.friendly.find(params[:id])
end
end
The create and update methods return multiple values, so I used the splat operator to DRY this up.
create_object and update_object don't follow the Rails default, so we need to implement those actions for Inherited Resources instead. Currently they don't seem to be handling validation errors. If you can, refactor them to use ActiveRecord's save and update, it would make everything even easier and DRYer.
I use ActionMailer to send different notifications to user. I use Model callbacks for this.
When I do changes as admin, I don't want any email to be sent to client.
How can I disable ActionMailer in RailsAdmin?
Actually, I'd like to provide an ability to admin to turn on/off emails.
Thanks
Triggering your mailers in the model lifecycle is not recommended IMHO. The recommended approach would be to trigger the mailers from the controller.
If you want to achieve separation of concerns in your controller and not pollute your controller code with mailer calls, you can use a combination of ActiveSupport::Notifications and controller after_filter to extract the mailer logic into its own module.
module MailerCallbacks
module ControllerExtensions
def self.included(base)
base.after_filter do |controller|
ActiveSupport::Notifications.instrument(
"mailer_callbacks.#{controller_path}##{action_name}", controller: controller
)
end
end
end
module Listener
def listen_to(action, &block)
ActiveSupport::Notifications.subscribe("mailer_callbacks.#{action}") do |*args|
event = ActiveSupport::Notifications::Event.new(*args)
controller = event.payload[:controller]
controller.instance_eval(&block)
end
end
end
end
Let's assume you want to refactor the following controller using our module created above:
class PostsController < ApplicationController
def create
#post = Post.new permitted_params
respond_to do |format|
if #post.save
PostMailer.notify(#post).deliver
format.html { redirect_to #post, notice: 'Successfully created Post' }
else
format.html { render action: 'new' }
end
end
end
end
Take the following steps:
Create an initializer to register the controller extension:
# config/initializers/mailer_callbacks.rb
ActiveSupport.on_load(:action_controller) do
include MailerCallbacks::ControllerExtensions
end
In the same or a separate initializer, create a class and extend the Listener module to register your callbacks:
# config/initializers/mailer_callbacks.rb
class MailerListeners
extend MailerCallbacks::Listener
# register as many listeners as you would like here
listen_to 'posts#create' do
PostMailer.notify(#post).deliver if #post.persisted?
end
end
Remove mailer code from your controller.
class PostsController < ApplicationController
def create
#post = Post.new permitted_params
respond_to do |format|
if #post.save
format.html { redirect_to #post, notice: 'Successfully created Post' }
else
format.html { render action: 'new' }
end
end
end
end
Essentially we have created an observer on the controller actions and registered our mailer callbacks with the controller instead of tying it into the model lifecycle. I personally consider this approach cleaner and easier to manage.
You should handle it on callback method.
def call_back_name
if is_rails_admin?
#Do nothing
else
#send mail
end
end
Let's separate this question into 2 parts:
Disable email
Check this answer
Integration with Rails Admin
You can add it to Rails Admin Navigation part and put it into separate controller for admin mailer turn on/off. More info here.
I have a comment model. I am creating a new instance of that model by passing it params from my view to the comment controller. Here is the comment controller:
class CommentsController < ApplicationController
def create
session[:return_to] = request.referrer
#comment = Comment.create(:user_id => current_user.id,
:issue_id => params[:issue_id],
:content => params[:content])
redirect_to session[:return_to]
end
end
Here is how I am passing the params in my view:
<%= link_to "Test Comment", comments_path(:issue_id => #issue.id,
:content => "HeLLO"),
method: :create %>
my question is - is this secure? What prevents someone from changing the params[:issue_id] and commenting on another issue? Is there a better way of doing this?
yeah, there are better ways
at first we look to your controller. to store the referrer and redirect back to it makes no sense (at least you should NOT save this in a session) rails can do this with the key :back.
at second you dont need to make a varaible with the # because you dont use the created object. and also you dont need to save the restult. just do
class CommentsController < ApplicationController
def create
Comment.create(:user=>current_user, :issue_id=>params[:issue_id],:content=> params[:content])
redirect_to :back
end
end
++ edit
actually a better way would to to it like this:
class CommentsController < ApplicationController
def create
current_user.comments.create(issue_id: params[:issue_id], content: params[:content])
redirect_to :back
end
end
just use rails associations
-- edit
and as you think, YES we can change the issue_id and write comments to any issue i want. so if you want to protect from this you have do do a helper before you crate a comment (its just an example)
class CommentsController < ApplicationController
def create
issue = Issue.find(params[:issue_id]
if issue.is_locked? || current_user.cant_write_at_issue(issue)
return redirect_to :back, :notice=>"You dont have Privilegs"
end
issue.comments.create :user=>current_user, :content=>params[:content])
redirect_to :back :notice=>"Comment was created successfully"
end
end
is_locked and cant_write_at_issue you need to define in your models. this is just a way how to protect something.
so now we can change the issue ID but you look if the user has access for doing this :-)
I'm learning Rails by building a shop application and I'm having a bit of trouble with redirects. I have 3 roles in the application:
Buyer
Seller
Administrator
Depending on which type they are logged in as then I would like to redirect to a different page/action but still show the same URL for each (http://.../my-account).
I don't like having to render partials in the same view, it just seems messy, is there another way to achieve this?
The only way I can think of is to have multiple actions (e.g. buyer, seller, administrator) in the accounts controller but that means the paths will look like http://.../my-account/buyer or http://.../my-account/seller etc.
Many thanks,
Roger
I've put my code below:
models/user.rb
class User < ActiveRecord::Base
def buyer?
return type == 'buyer'
end
def seller?
return type == 'seller'
end
def administrator?
return type == 'administrator'
end
...
end
controllers/accounts_controller.rb
class AccountsController < ApplicationController
def show
end
end
controllers/user_sessions_controller.rb
class UserSessionsController < ApplicationController
def new
#user_session = UserSession.new
end
def create
#user_session = UserSession.new(params[:user_session])
if #user_session.save
if session[:return_to].nil?
# I'm not sure how to handle this part if I want the URL to be the same for each.
redirect_to(account_path)
else
redirect_to(session[:return_to])
end
else
#user_session.errors.clear # Give as little feedback as possible to improve security.
flash[:notice] = 'We didn\'t recognise the email address or password you entered, please try again.'
render(:action => :new)
end
end
def destroy
current_user_session.destroy
current_basket.destroy
redirect_to(root_url, :notice => 'Sign out successful!')
end
end
config/routes.rb
match 'my-account' => 'accounts#show'
Many thanks,
Roger
In UserSessionsController#create (i.e.: the login method) you could continue to redirect to the account path (assuming that goes to AccountsController#show) and then render different views according to the role. I.e.: something like this:
class AccountsController < ApplicationController
def show
if current_user.buyer?
render 'accounts/buyer'
elsif current_user.seller?
render 'accounts/seller'
elsif current_user.administrator?
render 'accounts/administrator
end
end
end
Better yet, you could do this by convention...
class AccountsController < ApplicationController
def show
render "accounts/#{current_user.type}"
end
end
If I understand you question correctly, then the solution is simple.
You can just call the method you want inside your controller. I do this in my project:
def create
create_or_update
end
def update
create_or_update
end
def create_or_update
...
end
In your case it should be:
def action
if administrator? then
admin_action
elsif buyer? then
buyer_action
elseif seller? then
seller_action
else
some_error_action
end
end
You should probably explicitly call "render" with an action name in each of those actions, though.