I have a model named "MyModel" and it has attribute named "status".
I could update this attribute like this:
model.status = "new"
model.save!
However, I override this update method with my own model method "update_status". Any update through the default updating will be reject unless it uses "update_status"
What you can certainly do is track the changed attributes, and filter only those you want to allow to be updatable in before_update callback. In your update_status method you could use assignment through update_column which does not trigger callbacks.
before_update :prevent_status_update
private
def prevent_status_update
return true unless self.changed.includes? 'status'
self.errors.add_to_base "Cannot update a #{ self.to_s }"
false
end
Related
I have an action that simply toggles the #active attribute to the opposite boolean state:
If #blog.active == true then update it to inactive
If #blog.active == false then update it to active
I got the following custom action in a controller to work, but there has to be some Rails way to more elegantly do this:
class BlogsController < ApplicationController
...
def toggle_active
if #blog.active?
#blog.update(active: false)
else
#blog.update(active: true)
end
end
end
Is there a Rails way of updating a boolean attribute to the opposite boolean state?
ActiveRecord has the toggle and toggle! methods which do this. Just keep in mind that the toggle! method skips validation checks.
class BlogsController < ApplicationController
...
def toggle_active
#blog.toggle!(:active)
end
end
If you want to run validations you can do
#blog.toggle(:active).save
You can read the source code for the methods here
Active Record's toggle and toggle! methods handle toggling a Boolean attribute:
def toggle_active
#blog.toggle(:active).save
end
def toggle_active
#blog.toggle!(:active)
end
toggle changes the attribute, but doesn't save the change to the database (so a separate call to save or save! is needed).
toggle! changes the attribute and saves the record, but bypasses the model's validations.
Rails provides toggle for this, but it's not atomic, and you still have to call save and consider validations. The bang-method alternative, will handle persistence for you, but validations are skipped.
It's probably better to be explicit about what you're wanting to do here. If this action is the result of a form POST or an XHR request, accept the actual value you want to set (RESTful) instead of expecting the client to be aware of the current state of the database.
So while this works:
#blog.toggle(:active)
…it's non-deterministic and can result in the opposite of the desired action occurring. I would recommend this instead:
class BlogStatusController < ApplicationController
# ... boilerplate to load #blog
def update
#blog.update(status_params)
end
protected
def status_params
params.require(:blog).permit(status: [:active])
end
end
If you need an atomic toggle, you can easily achieve it with a SQL statement wrapped in a convenient method name: https://stackoverflow.com/a/24218418/203130
This should work:
#blog.update(active: !#blog.active?)
! is your friend:
!false # => true
!true # => false
You could try this:
#blog.update(active: !#blog.active?)
That's pretty compact.
I need to pass a value to attribute in a model from a different controller with no direct relation between them. In the below example I need to update farming_year in the Field Model from the Planting controller.
The Field model:
class Field < ApplicationRecord
has_many :crops
attr_accessor :farming_year
def getting_crops
#crops_list = Crop.select('crops.name').where(field_id: self.id, year: self.get_farming_year) # doesn't get the farming_year
end
def get_farming_year
#farming_year # passing the value directly will work #farming_year=2015!!
end
def farming_year=(val)
#farming_year = val # passing the value directly won't work #farming_year=2015!!
end
end
In the Planting controller:
def new
#field = Field.new
#field.farming_year = session[:working_year]
#field.save
flash.now[:success] = #field.get_farming_year # it works and gives the correct year
end
when I changed the #farming_year in the get_farming_year method to #farming_year=2016, then the code will work and will give the correct Crops records. the flash message in the code above without any change will give the correct year from the model. I think my main issue is passing the farming year from get_farming_year method to getting_crops method.
Hint: the framing year is belong to the Crop not to the Field, so I don't need to add it to the Field table.
Any ideas how to achieve that?
Your code has a number of issues.
attr_accessor
Why are you using an attr_accessor? You should store the value on a model attribute, in the database. If your Field model doesn't already have a farming_year attribute, create a migration to add it to the database by running these commands:
$ rails g migration AddFarmingYearToField farming_year:integer
$ rails db:migrate
If you're running Rails <= 4, use rake db:migrate instead of the second command.
Doing this means you don't need to use attr_accessor, or define getters and setters.
PlantingController#new
This method isn't working for you because you haven't defined the correct methods, and you're not saving the instance.
In your Field model, you've defined a farming_year method, but you haven't defined a farming_year= method, which is what the setter should be. Change your farming_year method definition to farming_year=. Alternatively, use the method I described in 1., then you won't have to.
Make sure you're saving the model object once you're done with it - call Field#save, which returns truthy on success and falsy on failure; or call Field#save!, which returns truthy on success and raises an exception on failure.
The main issue with my code was using attr_accessor which I didn't need it, so, I've replaced "attr_accessor :farming_year" with a class variable "##work_year =''", and updated the getter and setter method as in the below code
The Field model:
class Field < ApplicationRecord
has_many :crops
attr_accessor :farming_year
##work_year =''
def getting_crops
#crops_list = Crop.select('crops.name').where(field_id: self.id, year: farming_year) #now this can request the getter method and get the year
end
def farming_year # getter method
##work_year ||= ''
end
def farming_year=(val) #setter method
##work_year = val
end
end
In the Planting controller:
def new
#field = Field.new
#field.farming_year = session[:working_year]
##field.save NO need for this line
flash.now[:success] = #field.farming_year
end
Thank you all for your kind support:)
I have a field in my model for date created, this is not passed from the form and is currently set in the create method of my controller.
Should this be in my model instead in some sort of initializer method? If so what would that method look like?
I have other fields I want to set as a default each a record is created so I'm trying to find out where is the accepted standard place to put these. I'm starting to think it should be the model as if the model was ever called outside the controller it wouldn't have all this logic.
I generally create builders and never use directly the standard Rails method create.
The point is to gather all the logic in one place with particular cases etc...
Basically in controllers I end up calling the builders this way:
#my_model_instance = MyModelBuilder.new(current_user, params[:my_model]).build
#my_model_instance = MyModelBuilder.new(current_user, params[:my_model]).create
All my builders live in /app/builders
Here is a very basic example:
class MyModelBuilder
attr_accessor :params, :user, :my_model
# consider using a Struct if you keep a very basic initializer
def initialize(user, params)
self.user = user
self.params = params
end
def build
my_model
end
def create
my_model.tap{|m| m.save }
end
def my_model
#my_model ||= MyModel.new(default_values.merge(params))
end
def default_values
{
foo: 'bar'
}
end
end
Rails already manages the date of creation and update of your records.
If your model has a created_at field or an updated_at field they will be filled with the time of creation and update of your model.
You can generate those fields easily in a migration, for instance :
create_table :hello do
t.timestamps
end
Now, for default values, you can fill them in the initialize method of the model :
def initialize(*args)
self.field = default_value
super(*args)
end
Both of my callback methods have update_attributes in them. So it looks like when calculate_rating runs it also calls modify_rating. I only want calculate_rating to run for creating a new record and modify_rating to run only when editing and updating a record through a form.
after_create :calculate_rating
before_update :modify_rating
def calculate_rating
end
def modify_rating
end
From the fine manual for update_attributes:
Updates the attributes of the model from the passed-in hash and saves the record [...]
So when you call update_attributes, it will try to save the object and that means that update_attributes is not appropriate for either of the callbacks you're using; update_attributes is meant to be used by controllers for mass assignment and the like.
You could replace the update_attributes call with simple assignments:
def calculate_rating
self.attr1 = 11
self.attr2 = 23
#...
end
In my controller i want to dynamically bind my instance method to the before_save callbacks.
Is there any ways we can dynamically bind methods to the callback from controller side....
EDIT :
Controller
This original code..
def amfupdate
set_properties
validate_record if params[:csv_header][:validate_record] == "Y" #On this condition...
super
end
If condition is true than i want to set custom callback that will exact call after before_save but before object is saved.
I want to call this method exact after before_save.. But if condition is true on controller side ..
In Model
def validate_record
self.csv_columns.each do |csv_column|
self.errors.add_to_base(_("Invalid column name #{csv_column.column_name}.")) \
unless self.model_name.constantize.column_names.include?(csv_column.column_name)
end
end
I think you're looking for something like the following. In the Model:
validate do |instance|
instance.csv_columns.each do |csv_column|
instance.errors.add :csv_columns, "Invalid column name #{csv_column.column_name}"
unless instance.class.column_names.include?(csv_column.column_name)
end
end
This will be called before the record is saved and will abort the save if the errors are added to
UPDATE: With suggestion for conditional validations
Add an attribute to the model
attr_accessor :some_condtional
Set this in the controller
#instance.some_conditional = true # or false
Then the validation now looks like this:
validate do |instance|
instance.csv_columns.each do |csv_column|
instance.errors.add :csv_columns, "Invalid column name #{csv_column.column_name}"
unless instance.class.column_names.include?(csv_column.column_name)
end if instance.some_conditional
end
Or something along those lines. In other words use the model to hold the state and communicate the logic