Rails 3 : Mass-assignment with ActiveAdmin and has_one - ruby-on-rails

I am developing a rails application in which I have two models User and Client.
User is backed by devise and is responsible for authentication and has_one Client which holds the client details for a given user. This relation is always present as I ensure that a Client model is created whenever I create a User.
For the administration area I am using ActiveAdmin. Now, when I try to create a User through the administration interface I use a form like this:
form do |f|
f.inputs :username, :email, :password
f.inputs :name => "Client", :for => :client do |client|
client.inputs :name, :address, ...
end
end
The problem is that either the User nor the Client are saved and the page is reloaded with validation errors. I have checked rails console and there's a WARNING: Can't mass-assign protected attributes: client_attributes message every time I try to create a User.
I have searched over this issue and found that in order to allow for mass-assignment one had to define attr_accessible for each of the fields allowed for the assignment. So, I had put this directive in Client model for each of the fields mentioned above and the message keeps appearing, preventing the models to be properly saved.
Does anyone have a clue on this?

The problem is not in your Client model, but in your User model - because this is the primary model you are trying to create. All you need to do is to add client_attributes to the list of attr_accessible attributes in your User model, just as the error message in the log files says, e.g.:
class User < ActiveRecord::Base
attr_accessible :client_attributes
end
I imagine you already have a list of accessible attributes in the User class. So just add client_attributes to the end of that list.
The changes you made to your Client model (i.e. adding a list of attributes to attr_accessible) is not needed for this to work. If you want, you can also go ahead and undo that.

Related

Devise form validation in different forms

I have two different forms (two different controllers) and both these forms are using the Member model.
Devise uses it to create and update
MyController uses it to create and update
My model validates all fields as required.
So an example:
I have MyController presenting a form with all the fields (it works fine)
I have Devise presenting the register form with (email, password, password_confirmation).
When I try to submit the devise form, it shows me errors for fields that aren't there.
How can I skip validation for specific fields when Devise controller in the model?
It's good design to keep the devise model as small as possible and just keep it limited to user authentication. The reason being, every time you call current_user, it will load the whole object (with all the profile fields) whether or not you need them.
Since you've already created a new controller, I would suggest to create a new model for the profile attributes and create a has_one relationship with the devise model:
class User < ActiveRecord::Base
require 'devise'
has_one :user_profile
end
class UserProfile < ActiveRecord::Base
belongs_to :user
<user_profile fields>
end
With this approach your devise object will be lightweight and you can access the profile attributes only when required:
current_user.user_profile

Custom Active Admin form inputs for has_and_belongs_to_many relationship

I have a very simple model
class Lifestyle < ActiveRecord::Base
attr_accessible :name
has_and_belongs_to_many :profiles
end
that has a has_and_belongs_to_many relationship with Profile
class Profile < ActiveRecord::Base
attr_accessible ...
belongs_to :occupation
has_and_belongs_to_many :lifestyles
accepts_nested_attributes_for :lifestyles
end
I want to use ActiveAdmin to edit the Profile object, but also assign Lifestyles to a profile. It should be similar to dealing with belongs_to :occupation, as this is sorted out automatically by ActiveAdmin to a dropbox with the options pre-filled with available occupations.
I've tried to use the has_many form builder method, but that only got me to show a form to type in the name of the Lifestyle and on submission, it returned an error.
f.object.lifestyles.build
f.has_many :lifestyles do |l|
l.input :name
end
Error I get:
Can't mass-assign protected attributes: lifestyles_attributes
The perfect way for me would be to build several checkboxes, one for each Lifestyle in the DB. Selected means that the lifestyle is connected to the profile, and unselected means to delete the relation.
I'm having great doubts that this is possible using ActiveAdmin and without having to create very complex logic to deal with this. I would really appreciate it if you'd give your opinion and advise me if I should go this way or approach it differently.
After some research, I am ready to answer my own question.
First, I have to say thanks to #Lichtamberg for suggesting the fix. However, that only complicates things (also regarding security, though not an issue in this case), and doesn't help me reach my ideal solution.
Digging more, I found out that this is a very common scenario in Rails, and it's actually explained in Ryan Bates' screencast no #17.
Therefore, in Rails, if you have a has_and_belongs_to_many (short form HABTM) association, you can easily set the ids of the other associated object through this method:
profile.lifestyle_ids = [1,2]
And this obviously works for forms if you've set the attr_accessible for lifestyle_ids:
class Profile < ActiveRecord::Base
attr_accessible :lifestyle_ids
end
In ActiveAdmin, because it uses Formtastic, you can use this method to output the correct fields (in this case checkboxes):
f.input :lifestyles, as: :check_boxes, collection: Lifestyle.all
Also, I have simplified my form view so it's now merely this:
form do |f|
f.inputs # Include the default inputs
f.inputs "Lifestlyes" do # Make a panel that holds inputs for lifestyles
f.input :lifestyles, as: :check_boxes, collection: Lifestyle.all # Use formtastic to output my collection of checkboxes
end
f.actions # Include the default actions
end
Ok, now this rendered perfectly in the view, but if I try and submit my changes, it gives me this database error:
PG::Error: ERROR: null value in column "created_at" violates not-null constraint
: INSERT INTO "lifestyles_profiles" ("profile_id", "lifestyle_id") VALUES (2, 1) RETURNING "id"
I found out that this is due to the fact that Rails 3.2 doesn't automatically update the timestamps for a HABTM association table (because they are extra attributes, and Rails only handles the _id attributes.
There are 2 solutions to fix this:
Either convert the association into a hm:t (has_many, :through =>)
Or remove the timestamps from the table
I'm going to go for 2) because I will never need the timestamps or any extra attributes.
I hope this helps other people having the same problems.
Edit: #cdesrosiers was closest to the solution but I already wrote this answer before I read his. Anyway, this is great nevertheless. I'm learning a lot.
Active Admin creates a thin DSL (Domain-Specific Language) over formtastic, so it's best to look at the formastic doc when you need form customization. There, you'll find that you might be able to use f.input :lifestyles, :as => :check_boxes to modify a has_and_belongs_to_many relationship.
I say "might" because I haven't tried this helper myself for your particular case, but these things have a tendency to just work automagically, so try it out.
Also, you probably won't need accepts_nested_attributes_for :lifestyles unless you actually want to modify the attributes of lifestyles from profiles, which I don't think is particularly useful when using active admin (just modify lifestyles directly).
Add
attr_accessible :lifestyles_attributes
f.e.:
class AccountsController < ApplicationController
attr_accessible :first_name, :last_name
end

Any difference between :model and :model_id in attr_accessible for Rails/ActiveRecord?

When I create a scaffold and I need to have a belongs_to relation to another model, I add a field called model_id (replacing model with that model's name):
rails generate scaffold Grade user_id:integer subject_id:integer letter:string
Then in the above Grade model, I might add:
belongs_to :user
belongs_to :subject
Rails automatically adds user_id and subject_id to the list of attr_accessible fields. Do I do any harm by also adding :user and :subject to the list of attr_accessible fields so that I can mass assign using those as well?
attr_accessible is intended to protect against mass-assignment attacks that come from data that is externally sent to your application. In most cases you're probably doing things like this in your create & update actions:
#model = Model.new(params[:model])
or
#model.update_attributes(params[:model])
You should ask yourself why you'd have one form that uses subject_id and another that uses subject. The only real harm here is inconsistency, which can actually be pretty detrimental to large projects. If you follow the convention that all forms will use the actual database column (subject_id), then you'll save yourself some headache in the future when you can't remember out why you did it two different ways.
If you're updating attributes through the console, you can either use update_attributes(params[:model], without_protection: true) or a gem I wrote called sudo_attributes which lets you do sudo_update_attributes(params[:model]).
I think it doesn't hurt you, but will bring a kind of mess in your code

Dynamic scope for accessing Model Attributes

I'm currently using the mass assignment security baked into rails 3 to scope what level of users can update about their model. For example this code allows me to protect attributes based on the user level.
class Customer
attr_accessor :name, :credit_rating
attr_accessible :name
attr_accessible :name, :credit_rating, :as => :admin
end
I would like to be able to use this same idea for which attributes appear when I do a find. For example I would like to be able to say
Customer.all.as(:admin)
and get back the credit rating. Compare this to doing
Customer.all
and getting back all the attributes except the credit_rating
Is this something rails supports and I've missed?
attr_accessible is used to filter incoming attributes on mass assignment. This is a convenience method created so that a developer does not need to manually clean the incoming hash of params, something he does not control.
When displaying information a developer is in full control of what he/she desires to show, so there seems to be no reason to limit the read functionality.
However, rails allows you to "select" the attributes you desire in a query: see http://guides.rubyonrails.org/active_record_querying.html#selecting-specific-fields
You could easily create a scope with the name admin that would limit the selected values.
If you do not desire to have the full models, but only the values, you could use the generated sql. e:g.
ActiveRecord::Base.connection.select_values(Customer.select('name').to_sql)

Add fields to User model in Devise

I'm a bit of a newbie at Rails and feel I'm missing a trick here. I'm trying to add a phone_number field to my Devise-generated User model, but I'm having an issue with saving it. I've done the rails generate devise:views, updated the edit.html.erb file to add in the :phone_number field, and created a migration to add the phone_number field to the model. It's not saving to the model because (as I understand it) I can't update the controller to include the new fields.
Do I need to create an app/controllers/users/registration_controller.rb defined with class Users::RegistrationsController < Devise::RegistrationsController and then monkey patch the update method? Or is there a more straightforward/elegant/easier way?
I realize there are a couple other questions related to this on the site, but one offers no useful answers, and the other simply details what I mention here. Is there anything more to it?
Thanks.
After you add the field to your database through a migration you will also need to add it to your list of accessible attributes in your User model. Your attr_accessible list should look something like the following depending on what devise modules you are using.
attr_accessible :email, :password, :password_confirmation, :phone_number

Resources