In my Rails app I have invoices which in turn can have many projects.
model:
class Invoice < ActiveRecord::Base
attr_accessible :project_id
end
controller:
class InvoicesController < ApplicationController
before_filter :authorized_user, :only => [ :show, :edit, :destroy ]
before_filter :authorized_project, :only => [ :create, :update ]
def create # safe
#invoice = #project.invoices.build(params[:invoice])
if #invoice.save
flash[:success] = "Invoice saved."
redirect_to edit_invoice_path(#invoice)
else
render :new
end
end
def update # not safe yet
if #invoice.update_attributes(params[:invoice])
flash[:success] = "Invoice updated."
redirect_to edit_invoice_path(#invoice)
else
render :edit
end
end
private
def authorized_user
#invoice = Invoice.find(params[:id])
redirect_to root_path unless current_user?(#invoice.user)
end
def authorized_project
#project = Project.find(params[:invoice][:project_id])
redirect_to root_path unless current_user?(#project.user)
end
end
My biggest concern is that a malicious user might, one day, create an invoice that belongs to the project of another user.
Now thanks to the help of some people on this board I managed to come up with a before_filter that makes sure that this won't happen when a project is created.
The problem is I don't understand how to apply this filter to the update action as well.
Since the update action does not make use of Rails' build function, I simply don't know how to get my #project in there.
Can anybody help?
In your case I would start from current_user, not #project (provided User has_many :invoices):
current_user.invoices.build(params[:invoice])
Also instead of explicitly check current_user?(#invoice.user) you can do:
def find_invoice
#invoice = current_user.invoices.find(params[:id])
end
def find_project
#project = current_user.projects.find(params[:invoice][:project_id])
end
Wrong invoice or project will throw 500 which you may or may not want to handle.
If User has_many :invoices, :through => :projects and Project hence has_many :invoices then:
def find_invoice
#invoice = #project.invoices.find(params[:id])
end
The #project.invoices.build method creates a new Invoice that is automatically associated with that particular #project. You don't have to do any work, and there's no risk of it being linked to the wrong project.
You'll want to be sure that project_id is not an accessible attribute, though.
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 have a has_one association in my model with my user. What I'm trying to do here is simple but I'm having a hard time understanding whats wrong here. So since I have a has_one association with my model, in my mind I was simply thinking that if the user has already created the model associated with the has_one association if he tries accessing "localhost3000/model/new" I would redirect him to the edit page of this particular model. Here is what I have but its telling me its not working as intended. It's as if my if statement is not catching anything
class BusinessesController < ApplicationController
before_action :set_business, only: [:show, :edit, :update, :destroy]
def index
#businesses = Business.all
#buzzs = Buzz.all
end
def show
end
def new
if current_user.business.nil?
#business = current_user.build_business
else
render 'edit'
end
end
def edit
end
def create
#business = current_user.build_business(business_params)
if #business.save
redirect_to #business, notice: 'Business was successfully created.'
else
render "new"
end
end
This error does not make a lot of sense to me because it says its an error in the "new" controller which would have rendered it to the edit path thus not being nil
This is happening because you're not setting #business when redirecting to 'edit'. Try this:
def new
if current_user.business.nil?
#business = current_user.build_business
else
#business = current_user.business
render 'edit'
end
end
In my application I have a "bookings" table, and an "extras" table.
This is a many-many relationship. Therefore I have created a middle table called "additions"
I've used the "has_many :through" to establish the relationship between the tables:
class Booking < ActiveRecord::Base
has_many :additions
has_many :extras, :through => :additions
class Extra < ActiveRecord::Base
has_many :additions
has_many :extras, :through => :additions
class Addition < ActiveRecord::Base
belongs_to :booking
belongs_to :extra
This seems to work. I added a few extras to some existing bookings manually (by adding numbers to the additions table), and wrote code so that when you click to show a booking, it lists all associated extras.
Now I need to make it so that when you make a booking - the "extras" are saved into the middle (additions) table.
I have checkboxes on my bookings form page:
<%= f.label 'Extras:' %>
<%= f.collection_check_boxes :extra_ids, Extra.all, :id, :extra_info %>
But obviously, the choices just get discarded when the user clicks on save.
I need some code to go (in the controller?) to make it save these "extras" into the "additions table" ?
Any ideas, as I can't work out how to do this?!
Thanks!
class BookingsController < ApplicationController
respond_to :html, :xml, :json
before_action :find_room
# before_action :find_extra
def index
#bookings = Booking.where("room_id = ? AND end_time >= ?", #room.id, Time.now).order(:start_time)
respond_with #bookings
end
def new
#booking = Booking.new(room_id: #room.id)
end
def create
#booking = Booking.new(params[:booking].permit(:room_id, :start_time, :length, :user_id))
#booking.room = #room
if #booking.save
redirect_to room_bookings_path(#room, method: :get)
else
render 'new'
end
end
def show
#booking = Booking.find(params[:id])
end
def destroy
#booking = Booking.find(params[:id]).destroy
if #booking.destroy
flash[:notice] = "Booking: #{#booking.start_time.strftime('%e %b %Y %H:%M%p')} to #{#booking.end_time.strftime('%e %b %Y %H:%M%p')} deleted"
redirect_to room_bookings_path(#room)
else
render 'index'
end
end
def edit
#booking = Booking.find(params[:id])
end
def update
#booking = Booking.find(params[:id])
# #booking.room = #room
if #booking.update(params[:booking].permit(:room_id, :start_time, :length, :user_id))
flash[:notice] = 'Your booking was updated succesfully'
if request.xhr?
render json: {status: :success}.to_json
else
redirect_to resource_bookings_path(#room)
end
else
render 'edit'
end
end
private
def save booking
if #booking.save
flash[:notice] = 'booking added'
redirect_to room_booking_path(#room, #booking)
else
render 'new'
end
end
def find_room
if params[:room_id]
#room = Room.find_by_id(params[:room_id])
end
end
# def find_extra
# if params[:extra_id]
# #extra = Extra.find_by_id(params[:extra_id])
# end
# end
# If resource not found redirect to root and flash error.
def resource_not_found
yield
rescue ActiveRecord::RecordNotFound
redirect_to root_url, :notice => "Booking not found."
end
def booking_params
params.require(:booking).permit(:user_id, :extra_id)
end
end
------------------------
class AdditionsController < ApplicationController
before_action :set_addition, only: [:show, :edit, :update, :destroy]
# GET /additions
def index
#additions = Addition.all
end
# GET /additions/1
def show
end
# GET /additions/new
def new
#addition = Addition.new
end
# GET /additions/1/edit
def edit
end
# POST /additions
def create
#addition = Addition.new(addition_params)
if #addition.save
redirect_to #addition, notice: 'Addition was successfully created.'
else
render :new
end
end
# PATCH/PUT /additions/1
def update
if #addition.update(addition_params)
redirect_to #addition, notice: 'Addition was successfully updated.'
else
render :edit
end
end
# DELETE /additions/1
def destroy
#addition.destroy
redirect_to additions_url, notice: 'Addition was successfully destroyed.'
end
private
# Use callbacks to share common setup or constraints between actions.
def set_addition
#addition = Addition.find(params[:id])
end
# Only allow a trusted parameter "white list" through.
def addition_params
params.require(:addition).permit(:booking_id, :extra_id, :extra_name)
end
end
--------------------------------------
# #author Stacey Rees <https://github.com/staceysmells>
class ExtrasController < ApplicationController
# #see def resource_not_found
around_filter :resource_not_found
before_action :set_extra, only: [:show, :edit, :update, :destroy]
def index
#extras = Extra.all
end
def show
end
def new
#extra = Extra.new
end
def edit
end
def create
#extra = Extra.new(extra_params)
if #extra.save
redirect_to #extra, notice: 'Extra was successfully created.'
else
render :new
end
end
def update
if #extra.update(extra_params)
redirect_to #extra, notice: 'Extra was successfully updated.'
else
render :edit
end
end
def destroy
#extra.destroy
redirect_to extras_url, notice: 'Extra was successfully destroyed.'
end
private
# Use callbacks to share common setup or constraints between actions.
def set_extra
#extra = Extra.find(params[:id])
end
# If resource not found redirect to root and flash error.
def resource_not_found
yield
rescue ActiveRecord::RecordNotFound
redirect_to root_url, :notice => "Room Category not found."
end
# Only allow a trusted parameter "white list" through.
def extra_params
params.require(:extra).permit(:extraimg, :name, :description, :quantity, :price, :extracat_id)
end
end
What you're doing here is working with nested form attributes. It's a bit complex, but it's also something people do often, so there are some good resources available.
I suggest you look at this post: http://www.sitepoint.com/complex-rails-forms-with-nested-attributes/
In particular, the section named 'More Complicated Relationships' specifically has an example of using nested attributes to set up a many-to-many association using has_many :through.
The key pieces (which commenters have already pointed out) are going to be accepts_nested_attributes_for :extras in your Booking model, and a f.fields_for :extras block in the view. You'll also need to modify your booking_params method to permit the nested values. There are a couple of strong parameters gotchas that you can potentially run into with that, so you may need to review the documentation.
It turns out I was nearly there with the code I had once the accepts_nested_attributes_for was written in.
My main issue was setting up the booking_params method in the controller. I got it to work by declaring :extra_ids => [] in my params.permit.
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.
In my Rails app I have users who can have many projects which in turn can have many tasks.
model:
class Task < ActiveRecord::Base
attr_accessible :project_id
end
controller:
class TasksController < ApplicationController
def create
#task = current_user.tasks.build(params[:task])
if #task.save
flash[:success] = "Task saved."
redirect_to edit_task_path(#task)
else
render :new
end
end
def update
if #task.update_attributes(params[:task])
flash[:success] = "Task updated."
redirect_to edit_task_path(#task)
else
render :edit
end
end
end
What's the standard practice in Rails to ensure that a user A can not create a task for a user B?
Right now, I am restricting the project_ids that are available to a user through the select box options in the form. However, this can be easily hacked through a browser console and is not safe at all.
How can this be improved?
Thanks for any help.
I would go with a before filter that checks if required project belongs to current user :
class TasksController < ApplicationController
before_filter :find_project, only: :create
def create
#task = #project.tasks.build(params[:task])
if #task.save
flash[:success] = "Task saved."
redirect_to edit_task_path(#task)
else
render :new
end
end
private
def find_project
#project = current_user.projects.where( id: params[ :task ][ :project_id ] ).first
redirect_to( root_path, notice: 'No such project' ) unless #project
end
end
So, if given project_id does not match a project belonging to current user, he is redirected out.
A more rails way, though, would be to use nested resources :
resources :projects
resources :tasks, shallow: true
end
You would have routes like this :
GET /projects/1/tasks (index)
GET /projects/1/tasks/new (new)
POST /projects/1/tasks (create)
GET /tasks/1 (show)
GET /tasks/1/edit (edit)
PUT /tasks/1 (update)
DELETE /tasks/1 (destroy)
But this won't differ much, you still have to retrieve Post :
class TasksController < ApplicationController
before_filter :find_project, only: [ :index, :new, :create ]
before_filter :find_task, only: [ :show, :edit, :update, :delete ]
# other actions
def create
#task = #project.tasks.build(params[:task])
if #task.save
flash[:success] = "Task saved."
redirect_to edit_task_path(#task)
else
render :new
end
end
private
def find_project
#project = current_user.projects.where( id: params[ :project_id ] ).first
redirect_to( root_path, notice: 'No such project' ) unless #project
end
def find_task
#task = current_user.tasks.where( id: params[ :id ] ).first
redirect_to( root_path, notice: 'No such task' ) unless #task
end
end
The easiest thing to do is scope your lookup and exploit the fact that #find can raise RecordNotFound. Rails will rescue that exception and render 404 for you.
class TasksController < ApplicationController
helper_method :project
def create
#task = project.tasks.build(params[:task])
if #task.save
flash[:success] = "Task saved."
redirect_to edit_task_path(#task)
else
render :new
end
end
private
def project
#project ||= current_user.projects.find(params[:task][:project_id])
end
end
I would also add that you should also scope the URL for tasks under the project it belongs to. Something like /projects/:project_id/tasks/:id using nested resources.