My Invoice model has an address_id attribute, and I don't want this address_id to change FOREVER. So I don't want this to happen outside the class:
invoice.address_id = 1
invoice.address = some_address
Rails automatically adds this address_id attribute to the model from the invoice table, so how can I declare this attribute private/protected? Calling
attr_protected :address_id
is most likely not the solution since based on the documentation it only prevents mass assignments.
Thanks!
You want attr_readonly.
Not as pretty as a one liner, but code below should work (and you could always do some metaprogramming to write an 'immutable' method)
def address_id=(id)
if new_record?
write_attribute(:address_id, id)
else
raise 'address is immutable!'
end
end
Related
I have a User model.
And User has a field called Balance (which represents how much money he has in his account).
I clearly don't want to make it attr_accessible.
But I want to be able to change its value (say when I charge him for something).
How do I write a getter/setter method for this attribute?
#object.attribute = new_value
attr_accesible is a protection from mass assignment don't mess it with attr_accessor which creates getters and setters
Here is question about difference Difference between attr_accessor and attr_accessible
attr_accessible protects you from mass assignment, as used by update_attributes and similar.
It doesn't affect reading the value of that attribute at all, and it doesn't affect you calling the accessor directly. For example you could write
user.balance -= item.price
user.save!
Presuming that you have previously verified that this is indeed the correct action to take.
I want to add to an existing model some attributes that need not be persisted, or even mapped to a database column.
Is there a solution to specify such thing ?
Of course use good old ruby's attr_accessor. In your model:
attr_accessor :foo, :bar
You'll be able to do:
object.foo = 'baz'
object.foo #=> 'baz'
I was having the same problem but I needed to bootstrap the model, so the attribute had to persist after to_json was called. You need to do one extra thing for this.
As stated by apneadiving, the easiest way to start is to go to your model and add:
attr_accessor :foo
Then you can assign the attributes you want. But to make the attribute stick you need to change the attributes method. In your model file add this method:
def attributes
super.merge('foo' => self.foo)
end
In case anyone is wondering how to render this to the view, use the method arguments for the render method, like so:
render json: {results: results}, methods: [:my_attribute]
Please know that this only works if you set the attr_accessor on your model and set the attribute in the controller action, as the selected answer explained.
From Rails 5.0 onwards you could use attribute:
class StoreListing < ActiveRecord::Base
attribute :non_persisted
attribute :non_persisted_complex, :integer, default: -1
end
With attribute the attribute will be created just like the ones being persisted, i.e. you can define the type and other options, use it with the create method, etc.
If your DB table contains a matching column it will be persisted because attribute is also used to affect conversion to/from SQL for existing columns.
see: https://api.rubyonrails.org/classes/ActiveRecord/Attributes/ClassMethods.html#method-i-attribute
In my case I wanted to use a left join to populate custom attribute. It works if I don't add anything but I also want to be able to set the attribute on a new object and of course it doesn't exist. If I add attr_accessor then it always returns nil after a select. Here's the approach I've ended up with that works for setting on new object and retrieving from left join.
after_initialize do
self.foo = nil unless #attributes.key?("foo")
end
def foo
#attributes["foo"]
end
def foo=(value)
#attributes["foo"] = value
end
Lets say I have a working form that looks like the following
=form_for #survey do |f|
=f.text_field :name
=f.fields_for :questions do |question_fields|
=question_fields.text_field :question_text
=question_fields.fields_for :answers do |answer_fields|
=answer_fields.text_field :answer_text
Because different parts of the form can be added and updated by different users I need a way to get the user_id into each model before it is saved. I realize it is not mvc compliant to be able to access current_user inside the model, that being said I am left without a solution.
If I was only saving one object it would be simple enough to assign the current_user.id to the object in the controller, but given the deeply nested nature of this form that starts to look like an ugly solution.
Is there an expert/railsy way to handle this?
Rails 3.2, devise
Can't each of the objects simply steal the user_id from their "parent" relationship? This is a common pattern:
class Answer < ActiveRecord::Base
before_validation :assign_user_id
protected
def assign_user_id
# Don't attempt if the question is not defined,
# or the user_id field is already populated.
return unless (self.question or self.user)
self.user_id = self.question.user_id
end
end
This involves a bit of additional database activity to resolve the answer for each question, as creating it in a scope is not sufficient, but it makes it pretty much fool-proof.
What you probably want to do is stuff in the user_id parameter when creating each record. This means your create call needs to merge in a :user_id key where required. The nested helper doesn't do this by default, though, so if you're using that you may just leave it up to the assign method.
I would like to have a more systematic solution for myself to avoid mass assignment.
A typical situation is to remove id or user_id from params (submitted automatically via form) and replace it with current_user.id internally (in MyController#create).
A solution I can think of is to create object from params hash, then update_attributes (of parent and child objects) to replace sensitive attributes with internal values:
#user = User.create(:params[:user])
#user.update_attributes(:id => current_user.id)
#user.profile.update_attributes(:user_id => current_user.id)
#user.preference.update_attributes(:user_id => current_user.id)
Is there a shorter/more DRY way to say this?
If preference, profile etc. are child objects of user (created via build method), how can I write a method to look for their foreign keys for user and automatically replace them with the value I passed to parent?
Thank you.
This is what attr_protected and attr_accessible (documentation) are for. attr_protected will give you blacklist protection, while attr_accessible will protect using a whitelist.
While calling update_attributes right after a mass assignment would work, you're better off using the built in ways of protecting mass assignments as it won't require duplication of code every time you do a mass assignment on a model.
I've done this in an earlier project by using:
#user = User.create(params[:user]) do |user|
user.id = current_user.id
end
Would this work for you?
An alternative is to check out the docs and search for the :as for role based attributes.
Here's the code:
class M
include Mongoid::Document
field :name
end
params = { name: "foo", age: 20 }
M.create(params)
#=> #<M name: "My Name", age: 20>
Notice that age wasn't defined, yet it was saved.
This is problematic (potentially a source of DoS) because a malicious user can add any parameters in POST and unknown fields with a large string can sneak in. (e.g. name=foo&bogus=#{'x'*1000000})
So far, I couldn't find anything but attr_accessible, but it's not really great for Mongoid as you have to maintain the same field names in both field and attr_accessible all the time, in all models. Not DRY.
I think the attr_accessible API is great for ActiveRecord, because there a. you don't explicitly define fields in the models (DRY) and b. it's guaranteed there's no chance that a nonexistent field gets saved to RDB. But for Mongoid, I think there should be a better solution than attr_accessible.
Note that there's a global config setting allow_dynamic_fields but it's not about mass assignment so it's out of the scope in this discussion, however I think it should actually be a per-model macro and should also take care of mass-assignment.
How are you dealing with this problem?
I'm always using attr_accessible in models. I rarely found myself including all fields as accessible. Usually there are always a few fields that shouldn't be accessible for mass assignment. If you often need to include every attribute and you're concerned about duplication:
attr_accessible *fields.keys
What I have done to solve this issue, is use a before save callback in my model:
set_callback(:save, :before) do |doc|
(doc.attributes.keys - fields.keys).each { |f| doc.unset(f) }
end
This way, even if there are extra attributes they get removed before being saved.