I’m following the (now slightly outdated) Meet Rails 3 tutorial from PeepCode, and am having trouble getting a couple of the tutorial’s suggestions to work together with Rails 3.2.
The tutorial has you create a Role
model that belongs to a Project
:
class Role < ActiveRecord::Base
belongs_to :project
validates :project_id, :presence => true
attr_protected :project_id
end
The routes.rb file nests Role
resources such that you must work with a Role
in the context of a Project
:
resources :projects do
resources :roles
end
Note in the model code above, the tutorial advises you to use attr_protected
to protect the :project_id
field, because it can be set “more securely” by creating every Role
in the context of a project, like this in roles_controller.rb:
class RolesController < ApplicationController
⋮
def create
@role = project.roles.new(params[:role])
⋮
The problem is, the HTML form for creating a Role
, which is created with Formtastic, contains a project_id
field for selecting the project. Therefore, when project.roles.new(params[:role])
tries to use the parameters from the form to populate the new Role
object, it tries to set the project_id
using mass assignment, and fails with:
ActiveModel::MassAssignmentSecurity::Error in RolesController#create
Can’t mass-assign protected attributes: project_id
What is the accepted way to implement this? Was protecting the project_id
attribute a bad idea? Or is there some way to populate the new Role
with the form data without including project_id
?
If you are getting project
via params[:project_id]
rather than params[:role][:project_id]
you could actually be setting conflicting values anyway.
The reason Mass Assignment would want to protect this is to prevent a user entering in an arbitrary value for project_id
that could allow a project
that isn't under this users control. You have a couple of options.
If you had an authorative user
or account
attached to the object you could add in a before_save
callback, such as self.project_id = nil unless user.projects.find(project_id)
.
Since you don’t, I'd use the project_id
from the hash to find the project, and fall back to the route id (I'm not sure if it would be project_id
or just id
off the top of my head).
def create
user.
projects.
find(params[:role].delete(:project_id) || params[:project_id] || params[:id]).
create(params[:role])
The easiest thing would be to just drop the select box from the form, since they've selected a project when choosing to create a new role – it's a nested resource.