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.
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
First time poster, long time lurker here. I have a Users model and controller for a little video game application for Rails that I'm currently making. So I've read a couple of answers on here regarding this issue, but none of the answers really seem to have helped me. People have suggested adding a "user_id" column to my Users table, but my point of contention is, I thought the "user_id" was automatically made in Rails? Even if I use a user.inspect, I still see a user_id=7show up on the page. However, I still get the unknown attribute error when attempting to create a game and assign to the current user. Any help would be most appreciated in pinpointing the cause and solution to this. Thanks!
app/controllers/users_controller.rb:
class UsersController < ApplicationController
skip_before_filter :require_authentication, only: [:new, :create]
def index
#users = User.all
end
def show
end
def new
#user = User.new
end
def edit
#user = current_user
end
def create
#user = User.create!(user_params)
session[:user_id] = #user.id
redirect_to users_path, notice: "Hi #{#user.username}! Welcome to DuckGoose!"
end
def update
current_user.update_attributes!(user_params)
redirect_to users_path, notice: "Successfully updated profile."
end
def destroy
#user = User.find(params[:id])
#user.destroy
redirect_to users_url, notice: 'User was successfully destroyed.'
end
private
def user_params
params.require(:user).permit(:username, :firstname, :lastname, :email, :password, :password_confirmation)
end
end
app/config/routes.rb:
NkuProject::Application.routes.draw do
resources :users do
resources :games
end
resources :sessions
resources :games
get "sign_out", to: "sessions#destroy"
get "profile", to: "users#edit"
root to: "sessions#new"
end
app/controllers/games_controller.rb
class GamesController < ApplicationController
def new
#game = Game.new
end
def index
#games = Game.all
end
def destroy
#game = Game.find(params[:id])
#game.destroy
redirect_to games_url, notice: 'Game was successfully deleted.'
end
def create
#game = current_user.games.build(game_params)
if #game.save
redirect_to #game, notice: "Game successfully added"
else
render :new
end
end
def show
#game = Game.find(params[:id])
end
private
def game_params
params.require(:game).permit!
end
end
app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
# Prevent CSRF attacks by raising an exception.
# For APIs, you may want to use :null_session instead.
protect_from_forgery with: :exception
before_filter :require_authentication
def current_user
#current_user ||= User.find_by(id: session[:user_id]) if session[:user_id].present?
end
helper_method :current_user
def require_authentication
if current_user
true
else
redirect_to new_session_path
end
end
end
I'm sure I'm missing some code to put in for reference, but if I need anything else please let me know.
Looking at the way your controller actions are defined, I can safely say that User and Game have a 1-M relationship, i.e.,
class User < ActiveRecord::Base
has_many :games
end
class Game < ActiveRecord::Base
belongs_to :user
end
Now, based on that games table must have a field named user_id. Rails is not going to create it for you unless you specify it. You need to add field user_id in games table by creating a migration for the same. Right now, it doesn't seem like you have user_id foreign_key field in games table. Hence, the error while saving games record.
I'm wondering how can I print on the index of my project only the rooms with the :is_available column or the rooms table with the :true value (is boolean).
I can't figure out how to achieve this (Sorry but I'm new with Rails). Any advice will be very appreciate!
I've this error with my current code:
"ActiveRecord::RecordNotFound in RoomsController#home
Couldn't find Room without an ID"
Here is my rooms_controller code:
class RoomsController < ApplicationController
before_action :get_room, only: [:index, :home]
def index
end
def show
#room = Room.find(params[:id])
end
def home
if params[:set_locale]
redirect_to root_url(locale: params[:set_locale])
else
puts #rooms if Room.all(params[:is_available => :true])
end
end
def get_room
#rooms = Room.all
end
end
You already have got #rooms = Room.all, you just need to precise your query (from all to your is_available restriction).
def home
if params[:set_locale]
redirect_to root_url(locale: params[:set_locale])
else
puts #rooms.where(is_available: true)
end
end
Also, you should avoid using puts in your controller logic. Either pass variable to the view (you can change #rooms value or create new variable #available_rooms), respond_with it or log it using Rails.logger if you use puts as a debugging solution.
def index
end
def home
if params[:set_locale]
redirect_to root_url(locale: params[:set_locale])
elsif params[:is_available]
puts #rooms
end
end
def get_room
#rooms = Room.where(is_available: true)
end
Using puts in controller - not a good idea.Use view to show the data.
There are several issues you may have:
Routes
Your index method looks empty. I presume you're using "home" as a substitute
In this case, you have to know what type of action this is - a member or collection action? The reason this is important is that when you define your routes, you have to ensure you define the route in the right way. For your home route, I'd have done this:
#config/routes.rb
resources :rooms do
get "home", action: "home"
end
Scopes
You can use a scope to bring back all the values with :is_available present. This lives in the model like this:
#app/models/room.rb
Class Room < ActiveRecord::Base
scope :is_available?, -> { where(is_available: true) }
end
This will allow you to call
#room = Room.is_available?
Code
Although you've not given us any context of the error (when it happens, what you do to make it happen), this is what I would do to help fix it:
#app/controllers/rooms_controller.rb
def home
if params[:set_locale]
redirect_to root_url(locale: params[:set_locale])
else
puts Room.is_available?
end
end
This may change depending the params you send & how you send them
def home
if params[:set_locale]
redirect_to root_url(locale: params[:set_locale])
else
puts #rooms if params[:is_available] && Room.where(is_available: true)
end
end
should work.
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.
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.