rails - DRY up controller actions - ruby-on-rails

I have a controller with a lot of code duplication such as:
class PostController < ApplicationController
def action1
end
...
def actionN
end
end
And basically each action do something like this:
def action
#post = Post.find(params[:id])
if #post.action(current_user)
flash[:notice] = "#{custom string for this action}"
else
flash[:notice] = "Problem with your request"
end
redirect_to root_url
end
I thought about a method in ApplicationController that takes an array of symbols and generate the other methods, such as:
def self.action_for(*args)
args.each do |method, string|
define_method method.to_sym do
#post = Post.find(params[:id])
if #post.send method.to_sym
flash[:notice] = string
else
flash[:notice] = "Problem with your request"
end
redirect_to root_url
end
end
end
And call in PostController:
action_for [:action1, "Congratulations!"], [:action2, "Cool action!"] ..
I think this solution is ugly, it makes the ApplicationController dirty and allow other controllers to call my actions.
Any idea to solve the code-duplication problem?

Why don't you make a single action which will receive some extra parameter, like msg? Then you can take advantage of built-in I18n support:
def some_action
#post = Post.find(params[:id])
if #post.action(current_user)
flash[:notice] = I18n.t("messages.#{params[:msg]}", default: "Wrong message type")
else
flash[:notice] = I18n.t("messages.problem")
end
redirect_to root_url
end
Or maybe that makes sense to allow your #post.action to return some message for your notice?

I don't think there's anything too ugly in this solution.
To limit the logic to one controller, you can define self.action_for in PostController, instead of ApplicationController, and call it below its definition.
Note that you're already passing in first elements in pairs as symbols, so to_sym calls in action_for are not necessary.

Related

How should I write code if a params[:id] doesn't exist in the db

I'm writing a show action in a controller and want to branch if no id received from params[:id] exists in my database.
My app is still being created. I've tried conditional branching and exception handling. However, they didn't work as expected.
class ArticlesController < ApplicationController
#...
def show
begin
#article = Article.find(params[:id])
rescue
render root
end
end
#...
end
I expect to be redirect to the root if any error occurs.
The above code returns the record if it is found successfully. However, an ActiveRecord::RecordNotFound in ArticlesController#show occurs if no record is found.
How should I write code for this error?
How should I write code for this error?
The short answer is "you shouldn't".
Exceptions should, IMO, be exceptional. It sounds like you might expect this circumstance, so I suggest you write code that handles the scenario without raising an exception.
If you really think you'll have circumstances where the Article record does not exist for the given params[:id] (seems a little odd, but I guess it's possible), then I would suggest that you use .find_by instead of .find:
class ArticlesController < ApplicationController
#...
def show
if #article = Article.find_by(id: params[:id])
# do something with the article
else
render root
end
end
#...
end
It seems like to you might want to do a redirect instead of a render:
class ArticlesController < ApplicationController
#...
def show
if #article = Article.find_by(id: params[:id])
# do something with the article
else
redirect_to root_path
end
end
#...
end
But, maybe not...
Also, rescuing everything like this:
def show
begin
#article = Article.find(params[:id])
rescue
render root
end
end
...is generally viewed as code smell. There are a lot of articles on the interwebs about why.
You can handle the exception from ArticlesController but I advise put the code at
ApplicationController like this:
rescue_from ActiveRecord::RecordNotFound do
redirect_back fallback_location: root_path, alert: t("resource_not_found")
end
Maybe You can code like this:
def show
#article = Article.find_by(id: params[:id])
unless #article.present?
flash[:error] = "Not Found."
redirect_to root_path
end
end
why write so much code, while you can achieve with two lines only.
class ArticlesController < ApplicationController
def show
#article = Article.find_by(id: params[:id])
redirect_to root_path unless #article.present?
end
end
Your code should be written like this:
def show
begin
#article = Article.find(params[:id])
rescue
redirect_to root_path
end
end

Calling data from partial within controller without triggering multiple renders/redirects

I am trying to retrieve data from a partial within one of my controllers without tripping a redirect/render issue.
This is what the create method looks like within the controller, which is calling another function.
def create
#finding = #findings.new(finding_params)
respond_to do |format|
if #finding.save
if #project_notifications.where(category: "XXXXXXX").any?
notify_users(1)
end
flash[:notice] = 'Finding was successfully created.'
else
helpers.show_errors_for(#finding)
end
format.html { redirect_back fallback_location: root_path }
format.js { head :no_head }
end
end
The notify_users function looks like this:
def notify_users(notification_type)
if notification_type == 1
html_body = render(partial: 'findings/added_finding')
subject = "XXXXXXX."
#project_notifications.each do |notification|
NotificationWorker.perform_async(html_body, notification.user, subject)
end
end
end
The problem with this is that I trip a multiple render/redirect error. Is there a way to retrieve data from the partial without calling render a second time?
You can use render_to_string for this use case:
def notify_users(notification_type)
if notification_type == 1
html_body = render_to_string(partial: 'findings/added_finding', layout: false)
subject = "XXXXXXX."
#project_notifications.each do |notification|
NotificationWorker.perform_async(html_body, notification.user, subject)
end
end
end
That way you'll get a stringified representation of your HTML to use elsewhere. Let me know how you get on with this - I'm keen to know whether this serves your use case. And any questions, fire away :)
In your notify_users method (not function), you're calling render here:
html_body = render(partial: 'findings/added_finding')
And in your create method, you're calling redirect here:
format.html { redirect_back fallback_location: root_path }
So, it would seem that the statement:
Render and/or redirect were called multiple times in this action.
...is true.
I suppose you could move notify_user to a NotifyUserService and do the rendering there. When I render in services, I do not have the Render and/or redirect were call multiple times problem.
So, you might do something like:
#app/services/notify_user_service.rb
class NotifyUserService
include ActsAsRendering
class << self
def call(args={})
new(args).call
end
end
def initialize(args)
args.each do |k,v|
class_eval do
attr_accessor k
end
send("#{k}=",v)
end
end
def call
case notification_type
when :some_notification_type
html_body = render_partial('findings/added_finding')
subject = "XXXXXXX."
project_notifications.each do |notification|
NotificationWorker.perform_async(html_body, notification.user, subject)
end
end
end
def some_information
case notification_type
when :some_notification_type
'some notification information'
else
'default notification information'
end
end
end
Then, in your controller,
def create
#finding = #findings.new(finding_params)
respond_to do |format|
if #finding.save
if #project_notifications.where(category: "XXXXXXX").any?
NotifyUserService.call(
notification_type: :some_notification_type,
project_notifications: #project_notifications
)
end
flash[:notice] = 'Finding was successfully created.'
else
helpers.show_errors_for(#finding)
end
format.html { redirect_back fallback_location: root_path }
format.js { head :no_head }
end
end
This, naturally, assumes that NotifyUserService knows how to render. I forget what the state of play is with rendering anywhere in the Rails 5/6 world is. But, to tell my services how to render, I have a module called something like ActsAsRendering:
#app/acts_as/acts_as_rendering.rb
#------------------------------------------------------------------------------------------------
# Use like:
# include ActsAsRendering
#------------------------------------------------------------------------------------------------
module ActsAsRendering
module ClassMethods
end
module InstanceMethods
private
def action_view
#action_view ||= new_action_view
end
def new_action_view
av = ActionView::Base.new
av.view_paths = ActionController::Base.view_paths
av.class_eval do
include Rails.application.routes.url_helpers
include ApplicationHelper
end
av
end
def method_missing(meth, *params, &block)
if action_view.respond_to?(meth)
action_view.send(meth, *params, &block)
else
super
end
end
def render_partial(file_name)
file_name = file_name.to_s
render(partial: file_name, locals: {presenter: self})
end
end
def self.included(receiver)
receiver.extend ClassMethods
receiver.include InstanceMethods
end
end
You'll notice that I created a render_partial method and use that instead of render. The render_partial method passess the NotifyUserService instance in as a local. That way, in my views, I can do stuff like:
#app/views/findings/added_finding.html.haml
- #presenter = local_assigns[:presenter] if local_assigns[:presenter]
#some-document-id
.some-presenter-information
#presenter.some_information
And now the view will show 'some notification information' in the html. This way, I can push all view logic back into the presenter and my views become 100% logic free.

Rails: can ActiveRecord method "destroy" go wrong and not be performed?

I am writing an app in RoR, and I was wondering if following if statement should be put there:
class ArticlesController < ApplicationController
before_action :set_user, only: %i[destroy edit]
...
def destroy
if #article.destroy
flash[:success] = 'Article deleted'
redirect_to articles_path
else
flash[:error] = 'Failed to delete the article'
redirect_to article_path(#article)
end
end
...
private
def set_user
#article = Article.find(params[:id])
if current_user.id == #article.author_id
#author = current_user
else
redirect_to article_path(#article), message: 'You can\'t do that'
end
end
end
Is it possible for "else" branch i destroy method to be called? I am writing specs for that controller and I can't possibly think of the way to test that code.
I used the same approach as with ".save", but there we are dealing with DB validation. If there is no way "else" can be called, I will just delete those lines.
I something goes wrong with searching for the article owner or finding the article by id, error is gonna be present earlier, in "set_user" method.

is there any difference in using instance variables in a Rails controller other than visibility in the view

I tend to mix local variables and instance variables in Rails controllers when I don't need to use them in a view. Obviously, if I'm using in the view, I use instance variables. Is there any difference between using them in that scenario (other than class-level visibiity)?
For example:
def destroy
#micropost.find(params[:id])
#micropost.destroy
redirect_to root_url
end
or
def destroy
micropost.find(params[:id])
micropost.destroy
redirect_to root_url
end
an example of using instance variables for class level visibility would be here: https://github.com/mhartl/sample_app/blob/master/app/controllers/microposts_controller.rb ?
I think these lines of code is what your question about. Of course you don't have to instantiate that variable with # since you are not actually going to show it on your view(since it is being destroyed). The purpose of these lines of code if to first check wether #micropost exists, if it does not then redirect_to root_path else it will go to destroy method destroy the micropost and then redirect_to root_path.
Now, to answer your question, yes, there is a huge difference between #micropost and micropost. #micropost will be accessible in other methods of your controller while micropost will not(since its scope will be limited to the method you instantiate it in).
However, if you're concern about not having a # variable then you can change the code shown here to this:
class MicropostsController < ApplicationController
before_filter :signed_in_user
def create
#micropost = current_user.microposts.build(params[:micropost])
if #micropost.save
flash[:success] = "Micropost created!"
redirect_to root_path
else
#feed_items = []
render 'static_pages/home'
end
end
def destroy
if micropost = current_user.microposts.find_by_id(params[:id])
micropost.destroy
end
redirect_to root_path
end
end
Beside, scope difference, you can access instance variable in another method of same controller, as
def method1
#var = "some value"
puts #var
end
def method2
puts #var
end
now, depending on the sequence u call these method, #var can have different values

Rails: Keeping user spoofing checks DRY

In a fit of unoriginality, I'm writing a blog application using Ruby on Rails. My PostsController contains some code that ensures that the logged in user can only edit or delete their own posts.
I tried factoring this code out into a private method with a single argument for the flash message to display, but when I did this and tested it by editing another author's post, I got an ActionController::DoubleRenderError - "Can only render or redirect once per action".
How can I keep these checks DRY? The obvious approach is to use a before filter but the destroy method needs to display a different flash.
Here's the relevant controller code:
before_filter :find_post_by_slug!, :only => [:edit, :show]
def edit
# FIXME Refactor this into a separate method
if #post.user != current_user
flash[:notice] = "You cannot edit another author’s posts."
redirect_to root_path and return
end
...
end
def update
#post = Post.find(params[:id])
# FIXME Refactor this into a separate method
if #post.user != current_user
flash[:notice] = "You cannot edit another author’s posts."
redirect_to root_path and return
end
...
end
def destroy
#post = Post.find_by_slug(params[:slug])
# FIXME Refactor this into a separate method
if #post.user != current_user
flash[:notice] = "You cannot delete another author’s posts."
redirect_to root_path and return
end
...
end
private
def find_post_by_slug!
slug = params[:slug]
#post = Post.find_by_slug(slug) if slug
raise ActiveRecord::RecordNotFound if #post.nil?
end
The before filter approach is still an ok option. You can gain access to which action was requested using the controller's action_name method.
before_filter :check_authorization
...
protected
def check_authorization
#post = Post.find_by_slug(params[:slug])
if #post.user != current_user
flash[:notice] = (action_name == "destroy") ?
"You cannot delete another author’s posts." :
"You cannot edit another author’s posts."
redirect_to root_path and return false
end
end
Sorry for that ternary operator in the middle there. :) Naturally you can do whatever logic you like.
You can also use a method if you like, and avoid the double render by explicitly returning if it fails. The key here is to return so that you don't double render.
def destroy
#post = Post.find_by_slug(params[:slug])
return unless authorized_to('delete')
...
end
protected
def authorized_to(mess_with)
if #post.user != current_user
flash[:notice] = "You cannot #{mess_with} another author’s posts."
redirect_to root_path and return false
end
return true
end
You could simplify it more (in my opinion) by splitting out the different parts of behavior (authorization, handling bad authorization) like this:
def destroy
#post = Post.find_by_slug(params[:slug])
punt("You cannot mess with another author's post") and return unless author_of(#post)
...
end
protected
def author_of(post)
post.user == current_user
end
def punt(message)
flash[:notice] = message
redirect_to root_path
end
Personally, I prefer to offload all of this routine work to a plugin. My personal favorite authorization plugin is Authorization. I've used it with great success for the last several years.
That would refactor your controller to use variations on:
permit "author of :post"
The simple answer is to change the message to something that fits both: "You cannot mess with another author's posts."
If you don't like the ugly* return in that last solution, you can use an around filter and conditionally yield only if the user is authorized.
around_filter :check_authorization, :only => [:destroy, :update]
private
def check_authorization
#post = Post.find_by_slug(params[:slug])
if #post.user == current_user
yield
else
flash[:notice] = case action_name
when "destroy"
"You cannot delete another author's posts."
when "update"
"You cannot edit another author's posts."
end
redirect_to root_path
end
end
*-- that's my preference, though code-wise it's perfectly valid. I just find that style-wise, it tends to not fit.
I also should add I haven't tested this and am not 100% certain it would work, though it should be easy enough to try.

Resources