Can I use an if statement in my controller or is this bad practice?
In both my create and destroy actions for TracksController, I want to do something like this:
if Product
#product = Product.find(params[:product_id])
#track = #product.tracks.create(params[:track])
eslif Release
#Release = Release.find(params[:release_id])
#track = #release.tracks.create(params[:track])
end
Is there a better way to do this?
I'd do it via a before_filter callback:
class TracksController < AC
before_filter :ensure_track, :only => [ :create, :destroy ]
private
def ensure_track
if Product
#product = Product.find(params[:product_id])
#track = #product.tracks.create(params[:track])
elsif Release
#release = Release.find(params[:release_id])
#track = #release.tracks.create(params[:track])
end
end
end
So with this setup it's ensured that you have a #track instance variable in your create and destroy methods, cause ensure_track gets invoked before those two methods.
I'm not sure though, if the logic you're applying makes sense... Why do you want to test if a constant named Product exists and if not if a constant named Release does? Maybe the question should be if either params[:product_id] or params[:release_id] is present!?
But that's a different question :)
UPDATE: See Rails Action Controller Guide for filters.
I'd go further and suggest a more DRY approach to the before_filter:
class TracksController < ApplicationController
before_filter :get_track_parent, only: [ :create, :destroy ]
def create
#track = #parent.tracks.create(params[:track])
...
redirect_to #parent
end
private
def get_track_parent
if params[:product_id].present?
#parent = Product.find(params[:product_id])
elsif params[:release_id].present?
#parent = Release.find(params[:release_id])
end
end
end
I used parent because we were given a context for the model relationships but I assume there's a better term to describe the commonality between release and product wrt tracks.
Related
I am trying to create an app where I have Events and each event would have many sales. When a new sale is created it automatically gets an event ID it belongs to. Could somebody please review this and tell me if I am doing something wrong, because I think the way am creating simple_form for the nested model(Sale) is a bit incorrect. Also I am not sure if it should be this way or I ve done something wrong, but when I am accessing nested children the url looks like this
.../events/4/sales/1
.../events/3/sales/1
.../events/5/sales/1
but I would expect it to be like this ?!
.../events/4/sales/1
.../events/4/sales/2
.../events/4/sales/3
Here is my controller and model for Events
class Event < ApplicationRecord
has_many :sales, dependent: :destroy
end
.
class EventsController < ApplicationController
def index
#events = Event.all
end
def new
#event = Event.new
end
def create
#event = Event.new(event_params)
if #event.save
redirect_to #event
else
redirect_to events_path
end
end
def show
#event = Event.find(params[:id])
#sales = #event.sales
end
private
def event_params
params.require(:event).permit(:name, :comment, :event_disscount)
end
end
.
Here is my controller and model for Sales
class Sale < ApplicationRecord
belongs_to :event
has_many :sale_items
accepts_nested_attributes_for :sale_items, allow_destroy: true
end
.
class SalesController < ApplicationController
def new
#sale = Sale.new(event_id: params[:event_id])
#event = Event.find_by(id: params[:event_id])
end
def create
#event = Event.find(params[:event_id])
#sale = #event.sales.create(params[:sale].permit(:receipt_email))
if #sale.save
redirect_to #event
else
redirect_to new
end
end
end
routes.rb
Rails.application.routes.draw do
# For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
resources :events do
resources :sales
end
root 'events#index'
end
And this is how I use simple_form for sale(new)
<%= simple_form_for([#event, #sale]) do |f| %>
My main concern is the 'new' action in Sales controller, whats the best way to create a nested resource with the id of its parent, and then passing this object to the simple_form?!
Thank you in advance
Your question is too broad. Basically you're doing it all right, however, with some improvements on the code it will be easier to find possible problems.
is it correct the way I am creating new sale?
Some improvements over your SalesController:
Create private method sale_params which will sanitize input params from your form. You did it for events already - why not to do it here too?
Since that controller works in the scope of event, params[:event_id] is set for every action. So create a before_action filter which will set your #event variable.
Method create saves the model to the database, so calling save after it makes no sense.
In case of failure on saving #sale to the db redirecting to new is not reasonable. In that case everything user typed in the form will be lost, validation error won't be shown and it will look like a glitch of your app. Render new template instead with the same #sale.
This is how I would rewrite your controller:
class SalesController < ApplicationController
before_action: :set_event
def new
#sale = #event.sales.build
end
def create
#sale = #event.sales.build(sale_params)
if #sale.save
redirect_to #event
else
render action: :new
end
end
private
def sale_params
params.require(:sale).permit(:receipt_email, sale_items_attributes: [])
end
def set_event
#event = Event.find(params[:event_id])
end
end
I'm currently trying to refactor some controller code and I came accross some code that I'm not sure how to implement in a correct way.
My application has users and companies, and both can have projects.
The current situation is that we have 2 urls:
example.com/projects/*action (for user projects)
example.com/company/:company_id/projects/*action (for company projects)
Those will route to the same controller which will handle the request differently based on if a company_id exists or not. This is not very clean in my opinion so I have been thinking about a better way to do this.
So far, I think the best way is to split them up in seperate controllers, like:
Users::ProjectsController
Companies::ProjectsControler
But since the only difference between a user project and a company project is pretty much that one has a 'user_id' and the other has a 'company_id', it feels like that will not be very DRY as I'll be writing a lot of duplicate code.
The current solution probably isn't as much of a problem, but I want to do this the correct way, so was hoping that someone over here would have a suggestion on how to handle this.
Thanks in advance!
EDIT:
This is how my ProjectsController#create currently looks
def create
if params[:company_id]
company = current_user.get_companies.find(params[:company_id])
#project = Project.new(project_params)
#project.company_id = company.id
else
#project = Project.new(project_params)
#project.user_id = current_user.id
end
#project = Project.new(project_params)
if #project.save
flash[:notice] = "Project '#{#project.name}' created."
if #project.company
redirect_to company_project_path(#project.company, #project)
else
redirect_to project_path(#project)
end
else
flash[:error] = #project.errors.full_messages
if params[:company_id]
redirect_to new_company_project_path(params[:company_id], #project)
else
redirect_to new_project_path(#project)
end
end
end
It's mainly the if/else logic I'd like to get rid off
So i should probably just add company_id and user_id to the permitted_params and let use a function to put either one of them in the params...
Because you said the only difference is associating company_id vs user_id, as #TomLord said, you might find something like below work for you:
Assuming that you are using shallow nested routes:
class ProjectsController < ApplicationController
COLLECTION_ACTIONS = [:index, :new, :create].freeze
MEMBER_ACTIONS = [:show, :edit, :update, :destroy].freeze
before_action :set_associated_record, only: COLLECTION_ACTIONS
before_action :set_project, only: MEMBER_ACTIONS
def index
#projects = #associated_record.projects
# ...
end
def new
#project = #associated_record.projects.new
# ...
end
def create
#project = #associated_record.projects.new(project_params)
# ...
end
private
def set_project
#project = Project.find(params[:id])
end
def set_company
if params[:company_id].present?
#company = Company.find(params[:company_id])
end
end
# you might want to remove this set_user method, because perhaps you are already setting #user from sesssion
def set_user
if params[:user_id].present?
#user = User.find(params[:user_id])
end
end
def set_associated_record
set_company
set_user
#associated_record = #company || #user
end
end
Okay I managed to handle it in a way that I'm happy with.
ProjectsController#create now looks like this:
def create
#project = owner.projects.new(project_params)
if #project.save
flash[:notice] = "Project '#{#project.name}' created."
redirect_to action: :show, id: #project.id
else
flash[:error] = #project.errors.full_messages
#project = Project.new(project_params)
render action: :new
end
end
def owner
if params[:company_id]
return policy_scope(Company).find(params[:company_id])
else
return current_user
end
end
I added the owner class to return the entity that the project belongs to.
Any suggestions for improvements are still welcome though!
I am building an application that allows users to create a trip. However, for some reason I am not sure if I am truly utilizing the power of rails or am I being redundant in my code. In the following example you will see a trip controller where a trip can be created and then displayed. Everything works, I just want to make sure I am going about it in the most minimal fashion.
class TripsController < ApplicationController
def new
#user = User.find(session[:id])
#trip = Trip.new
end
def create
#trip = Trip.create(trip_params)
#user = User.find(session[:id])
redirect_to user_trip_path(#user.id, #trip.id)
end
def show
#trip = Trip.find(params[:id])
end
private
def trip_params
params.require(:trip).permit(:where, :when, :price_per_person)
end
end
To tighten it up, "scope the trip to the user".
class TripsController < ApplicationController
before_filter :find_user
def new
#trip = #user.trips.build #assuming a User has many trips
end
def create
#trip = #user.trips.create(trip_params) #you may want to add an if else here to catch bad trips
redirect_to user_trip_path(#user.id, #trip.id)
end
def show
#trip = #user.trips.find(params[:id])
end
private
def trip_params
params.require(:trip).permit(:where, :when, :price_per_person)
end
def find_user
#user = User.find(session[:id]) # or current_user if you are using popular authentication gems
end
end
It's about readability too, not just less lines.
I'm building a StackOverflow-like clone for studying purposes. Users have this ability to vote for someone's question, bringing its score up or down. My method works fine, however the repetition and the amount of controller logic are bothering me.
User has_many Votes
Question has_many Votes
Votes belong to Question/User
routes:
concern :voteable do
post 'votes/voteup', to: 'votings#voteup', as: :voteup
post 'votes/votedown', to: 'votings#votedown', as: :votedown
end
resources :questions, concerns: [:commentable, :favoriteable, :voteable] do
...
end
votes controller
class VotingsController < ApplicationController
def voteup
#question = Question.find(params[:question_id])
unless #question.user == current_user # checks if the user is the author
if current_user.voted?(#question.id) #checks if user already voted
#vote = current_user.votes.find_by(question_id: #question)
#vote.update_attributes(score: 1)
else
#vote = Vote.create(user: current_user, question: #question, score: 1)
end
end
redirect_to :back
end
def votedown
#question = Question.find(params[:question_id])
unless #question.user == current_user
if current_user.voted?(#question.id)
#vote = current_user.votes.find_by(question_id: #question)
#vote.update_attributes(score: -1)
else
#vote = Vote.create(user: current_user, question: #question, score: -1)
end
end
redirect_to :back
end
end
voted? is a method I've extracted to my User model
def voted?(question)
true if self.votes.where(question_id: question)
end
I would like to get rid of repetition in these two methods, but how?
Should I create one method like VOTE and one route leading to it with specified params (up/down) and then assign the score based on if/else? Sounds dirty to me, but that is the only thing that comes into my mind. I'm sure there has to be a beautiful Rails-way solution to this.
The User class could hold a method called vote_for(question, score) and defined like this:
# class User
def vote_for(question, score)
vote = self.votes.where(question_id: question.id).first || Vote.new(user: self, question: question)
vote.score = score
vote.save
end
class VotingsController < ApplicationController
before_filter :set_question, only: %w(voteup votedown)
def voteup
current_user.vote_for(#question, 1)
end
def votedown
current_user.vote_for(#question, -1)
end
protected
def set_question
#question = Question.find(params[:id])
end
A little tip: You should refactor your .voted? method of the User class to this:
def voted?(question)
self.votes.exists?(question_id: question)
end
This will either return TRUE or FALSE, not a complete object retrieved from the DB and then translated into a Ruby object.
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.