DRY voting methods - ruby-on-rails

I have a prompt asking to write my up_vote and down_vote methods in only two lines using 'redirect_to' and the 'update_vote!' method as presented below. Implementing redirect_to is easy enough, but I'm not quite sure how to write my up/down_vote methods concisely using the existing 'update_vote!' method. Any help is appreciated.
class VotesController < ApplicationController
before_action :load_post_and_vote
def up_vote
if #vote
#vote.update_attribute(:value, 1)
else
#vote = current_user.votes.create(value: 1, post: #post)
end
# http://apidoc.com/rails/ActionController/Base/redirect_to
redirect_to :back
end
def down_vote
if #vote
#vote.update_attribute(:value, -1)
else
#vote = current_user.votes.create(value: -1, post: #post)
end
# http://apidoc.com/rails/ActionController/Base/redirect_to
redirect_to :back
end
private
def load_post_and_vote
#post = Post.find(params[:post_id])
#vote = #post.votes.where(user_id: current_user.id).first
end
def update_vote!(new_value)
if #vote
authorize #vote, :update?
#vote.update_attribute(:value, new_value)
else
#vote = current_user.votes.build(value: new_value, post: #post)
authorize #vote, :create
#vote.save
end
end
end

You should invoke the update_vote! method. How about:
def up_vote
update_vote!(1)
# http://apidoc.com/rails/ActionController/Base/redirect_to
redirect_to :back
end
def down_vote
update_vote!(-1)
# http://apidoc.com/rails/ActionController/Base/redirect_to
redirect_to :back
end
Also your method can be re-written as :
def update_vote!(new_value)
Vote.find_or_create_by post: #post do |v|
authorize v, :update?
v.user_id = current_user.id
v.value = new_value
v.save!
end
end
Read find_or_create_by.

seems that answer was easy enough =]
def up_vote
update_vote(1)
redirect_to :back
end
def down_vote
update_vote(-1)
redirect_to :back
end

Related

What's a good approach to clean up (DRY) this controller?

I have an action being performed on every create, update and update_status methods on my controller, But I feel that I am repeating myself, and would really appreciate some help on a better approach to write this.
I've abstracted the update logic to a service, but the parameters are different, on every method of my controller.
def create
#story = Story.new(story_params)
#story.creator = current_user
if #story.save
next_state = StoryStateService.new(#story, current_user, nil).call
if next_state
#story.update_column(:status_id, next_state)
end
redirect_to stories_path
else
render 'stories/new'
end
end
def update
#story = Story.find(params[:id])
if #story.update(story_params)
next_state = StoryStateService.new(#story, current_user, nil).call
if next_state
#story.update_column(:status_id, next_state)
end
redirect_to stories_path
else
render 'edit'
end
end
def update_story_status_event
story = Story.find(params['story_id'])
sub_action = params['sub_action']
next_state = StoryStateService.new(story, current_user, sub_action).call
if next_state
story.update_column(:status_id, next_state)
end
redirect_to stories_path
end
As you can see, I have
next_state = StoryStateService.new(story, current_user, sub_action).call
if next_state
story.update_column(:status_id, next_state)
end
being repeated on three methods, but on regular create and update, I dont need to send a sub_action param (string).
I would just extract the advance of the story, anything more than that would be overkill to me, given the simplicity of everything else.
def create
#story = Story.new(story_params)
#story.creator = current_user
if #story.save
advance_story #story
redirect_to stories_path
else
render 'stories/new'
end
end
def update
#story = Story.find(params[:id])
if #story.update(story_params)
advance_story #story
redirect_to stories_path
else
render 'edit'
end
end
def update_story_status_event
story = Story.find(params['story_id'])
sub_action = params['sub_action']
advance_story story, sub_action
redirect_to stories_path
end
private
def advance_story(story, sub_action = nil)
next_state = StoryStateService.new(story, current_user, sub_action).call
if next_state
story.update_column(:status_id, next_state)
end
end
I am not sure what StoryStateService is doing, but if it is dealing with updating a number of models, then you can also make a module or class and put it in the lib directory.
The other option is (this may be more preferable for Story.find()) is to use before_action callback. (https://apidock.com/rails/v4.0.2/AbstractController/Callbacks/ClassMethods/before_action)
e.g.
As you are using Story.find() in update and update_story_status_event you can do the following:
before_action :find_story, :only => [:update, :update_story_status_event]
def find_story
#story = Story.find(params[:id])
rescue ActiveRecord::RecordNotFound
# handle error here
end

How does || work in?: #client = client.find(params[:client_id] || params[:id])

New to rails. Following a tutorial on polymorphic associations, I bump into this to set #client in create and destroy.
#client = Client.find(params[:client_id] || params[:id])
I'm normally only used to that you can only find #client = Client.find(params[:id])
so how does this work with there being two params? How does the || work?
FavoriteClientsController.rb:
class FavoriteClientsController < ApplicationController
def create
#client = Client.find(params[:client_id] || params[:id])
if Favorite.create(favorited: #client, user: current_user)
redirect_to #client, notice: 'Leverandøren er tilføjet til favoritter'
else
redirect_to #client, alert: 'Noget gik galt...*sad panda*'
end
end
def destroy
#client = Client.find(params[:client_id] || params[:id])
Favorite.where(favorited_id: #client.id, user_id: current_user.id).first.destroy
redirect_to #client, notice: 'Leverandøren er nu fjernet fra favoritter'
end
end
Full code for controller, models can be seen here
Using rails 5
Expression: params[:client_id] || params[:id] is the same as:
if params[:client_id]
params[:client_id]
else
params[:id]
end
Wow thats an incredibly bad way to do it.
A very extendable and clean pattern for doing controllers for polymorphic children is to use inheritance:
class FavoritesController < ApplicationController
def create
#favorite = #parent.favorites.new(user: current_user)
if #favorite.save
redirect_to #parent, notice: 'Leverandøren er tilføjet til favoritter'
else
redirect_to #parent, alert: 'Noget gik galt...*sad panda*'
end
end
def destroy
#favorite = #parent.favorites.find_by(user: current_user)
redirect_to #parent, notice: 'Leverandøren er nu fjernet fra favoritter'
end
private
def set_parent
parent_class.includes(:favorites).find(param_key)
end
def parent_class
# this will look up Parent if the controller is Parents::FavoritesController
self.class.name.deconstantize.singularize.constantify
end
def param_key
"#{ parent_class.naming.param_key }_id"
end
end
We then define child classes:
# app/controllers/clients/favorites_controller.rb
module Clients
class FavoritesController < ::FavoritesController; end
end
# just an example
# app/controllers/posts/favorites_controller.rb
module Posts
class FavoritesController < ::FavoritesController; end
end
You can then create the routes by using:
Rails.application.routes.draw do
# this is just a routing helper that proxies resources
def favoritable_resources(*names, **kwargs)
[*names].flatten.each do |name|
resources(name, kwargs) do
scope(module: name) do
resource :favorite, only: [:create, :destroy]
end
yield if block_given?
end
end
end
favoritable_resources :clients, :posts
end
The end result is a customizable pattern based on OOP instead of "clever" code.
The tutorial which teaches you to do
Client.find(params[:client_id] || params[:id])
is a super-duper bad tutorial :) I strongly recommend you to switch to another one.
Back to the topic: it is logical OR: if first expression is neither nil or false, return it, otherwise return second expression.
That thing is just trying to find client by client_id if there is one in the request params. If not it's trying to find client by id.
However such practic can make you much more pain than profit.

Fat Model, Skinny Controller in Rails with example

I really want to start learning Rails best practices, especially following the "fat model, skinny controller" logic.
Say I have the following comment controller
class CommentsController < ApplicationController
def create
#post = Post.find(params[:post_id])
#comment = #post.comments.create(comment_params)
#comment.user_id = current_user.id if current_user
#comment.save!
if #comment.save
redirect_to post_path(#post)
else
render 'new'
end
end
def edit
#post = Post.find(params[:post_id])
#comment = #post.comments.find(params[:id])
end
def update
#post = Post.find(params[:post_id])
#comment = #post.comments.find(params[:id])
if #comment.update(params[:comment].permit(:comment))
redirect_to post_path(#post)
else
render 'Edit'
end
end
def destroy
#post = Post.find(params[:post_id])
#comment = #post.comments.find(params[:id])
#comment.destroy
redirect_to post_path(#post)
end
private
def comment_params
params.require(:comment).permit(:comment)
end
What's a good place to start refactoring the code?
Immediately I think I an make the #post and #comment in both edit and update into a separate method, follow by calling a before_action on the method. But that is still putting all the code in the controller.
Are there any code that I can move to the model? If so, how should I structure them?
This code doesn't have much room for improvement, it's a basic crud, here's an example of a before_action like you suggested
before_action :load_post_and_comment, only: %i(edit update destroy)
def load_post_and_comment
#post = Post.find(params[:post_id])
#comment = #post.comments.find(params[:id])
end
And here a couple of other notes
def create
# ...
#comment.save!
if #comment.save
# ...
else
# ..
end
end
In this codition the you should remove the extra #comment.save! you only need to save once.
def update
# ...
if #comment.update(params[:comment].permit(:comment))
# ...
else
# ...
end
end
You already have the comment_params method, use it, because if you at any point add a new attribute to the comment, you'll update that method but you'll probably forget this part and you'll get werid errors till you notice that you need to permit here too.
If you want to really go all out with the skinny controller model, there is this gem: https://github.com/NullVoxPopuli/skinny_controllers
Where, you'd configure your CommentsController like so:
class CommentsController < ApplicationController
include SkinnyControllers::Diet
def create
if model.errors.present?
render 'new'
else
redirect_to post_path(model)
end
end
def update
redirect_to post_path(model)
end
# ... etc
private
def comment_params
params.require(:comment).permit(:comment)
end
end

Trouble Creating and showing Users in Rails blog

I keep getting a variety of error while trying to create and show errors in a simple Rails blog I'm trying to create.Let me know if you see anything obvious or if you need me to post more code as I've tried a number of things but to no avail. Thanks
The browser is giving me this error
Couldn't find User without an ID
in my "logged_in?" method which shows
def logged_in?
#current_user ||= User.find(session[:user_id])
end
Sessions Controller
class SessionsController < ApplicationController
def new
end
def create
user = User.find_by(email: params[:email])
if user && user.authenticate(params[:password])
session[:user_id] = user.id
flash[:success] = "You are logged in"
redirect_to root_path
else
render action: 'new'
flash[:error] = "There was a problem logging in. Please check your email and password"
end
end
end
def index
#users = User.all
end
def show
end
def new
#user = User.new
end
def edit
end
def create
#user = User.new(user_params)
if #user.save
session[:user_id] = #user.id
flash[:notice] = "You have registered, please login"
redirect_to login_path
else
render :new
end
end
def update
respond_to do |format|
if #user.update(user_params)
format.html { redirect_to #user, notice: 'User was successfully updated.' }
format.json { head :no_content }
else
format.html { render action: 'edit' }
format.json { render json: #user.errors, status: :unprocessable_entity }
end
end
end
def destroy
#user.destroy
respond_to do |format|
format.html { redirect_to users_url }
format.json { head :no_content }
end
end
private
def set_user
#user = User.find(params[:id])
end
def user_params
params.require(:user).permit(:first_name, :last_name, :email, :password, :password_confirmation)
end
end
Articles Controller
class ArticlesController < ApplicationController
http_basic_authenticate_with name: "dhh", password: "secret", except: [:index, :show]
def new
#article = Article.new
end
def index
#article = Article.all
end
def create
#article = Article.new(article_params)
if #article.save
redirect_to #article
else
render 'new'
end
end
def edit
#article = Article.find(params[:id])
end
def update
#article = Article.find(params[:id])
if #article.update(article_params)
redirect_to #article
else
render 'edit'
end
end
def show
#article = Article.find(params[:id])
end
def destroy
#article = Article.find(params[:id])
#article.destroy
redirect_to articles_path
end
private
def article_params
params.require(:article).permit(:title, :text, :image)
end
end
Application Helper
module ApplicationHelper
def logged_in?
#current_user ||= User.find(session[:user_id])
end
end
The problem you're facing is that session[:user_id] is nil. Usually a method which sets current user is called current_user. The logged_in? is not a good name for a method setting an user instance, because one would expect that a method ending with a question mark would return a true or false. And not an user instance.
Also, setting the current user is usually done with a before_filter. Additionally, you want to skip such before filter for action where you're setting the current user (i.e the current_user doesn't exist yet)
Finally, I would rather fail gracefully, if user is not found. You can achieve this by changing your code to User.find_by_id(session[:user_id])
While the user is not loggued, session[:user_id] is nil, and so User.find(session[:user_id]) generates the error. The method should be like this:
def logged_in?
#current_user ||= User.find(session[:user_id]) if session[:user_id].present?
end
Why would the logged_in? helper method try to assign a value to #current_user? I think that is a bad logic, it should just return a boolean result without modifying such a central instance. This is a proper way to do that:
def logged_in?
#current_user.nil? ? false : true
end
The responsibility of setting the #current_user falls to a method that you should place in application_controller.rb and make it a before_action so that it's executed before any controller action is triggered, that is:
# app/controllers/applicaton_controller.rb
class ApplicationController < ActionController::Base
protect_from_forgery
before_action :authenticate_user
# Your actions here
..
..
#
private
def authenticate_user
#current_user ||= User.find(session[:user_id]) if session[:user_id].present?
end
end

How can I use acts_as_commentable for two models

I want to use acts_as_commentable for two models (blog and post). I generated the comment model and added all my fields.
I created a comments_controller and in the create action I have to find blogs and posts, so for that to work I am doing something like this :-
def create
if controller_name == "blogs"
#blog = Blog.find(params[:comment][:blog_id])
#blog_comment = #blog.comments.new(:comment => params[:comment][:comment], :user_id => current_user.id)
if #blog_comment.save
flash[:success] = "Thanks for commenting"
redirect_to :back or root_path
else
flash[:error] = "Comment can't be blank!"
redirect_to :back or root_path
end
end
if controller_name == "topics"
#post = Post.find(params[:comment][:post_id])
#post_comment = #post.comments.new(:comment => params[:comment][:comment], :user_id => current_user.id)
if #post_comment.save
flash[:success] = "Thanks for commenting"
redirect_to :back or root_path
else
flash[:error] = "Comment can't be blank!"
redirect_to :back or root_path
end
end
I know it is pretty ugly but I don't know how to go ahead with this one, can anyone help me out?
I encountered the similar problem on one of my apps.
Here is what I've done:
class CommentsController < ApplicationController
before_filter :get_commentable
def create
#comment = #commentable.comments.build(params[:comment])
if #comment.save
...
else
...
end
end
private
def get_commentable
params.each do |k, v|
return #commentable = $1.classify.constantize.find(v) if k =~ /(.+)_id$/
end
end
end

Resources