Overriding active-record attributes method - ruby-on-rails

I wanted to overwrite the default active-record attributes method because i dont want to return created_at and updated_at in my json responses of any model.
so here's what i have done.
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
def attributes
super.except('created_at', 'updated_at')
end
end
This worked fine for me for the past few months. But now i came across a scenario that i should not send the password attribute from my User model. So
class User < ApplicationRecord
def attributes
super.except('password')
end
end
This worked like a charm when i run it from rails console. But when i run it from a controller, i really don't know for what reason, but it goes for a infinite loop. And here is my controller action.
def update
#object = klass.find(id)
#object.update_attributes!(update_params)
render json: {
status: true,
message: 'Saved Successfully..!',
data: object_json(#object)
}
end
def object_json(object)
object.as_json.except('updated_at', 'created_at')
end
Can someone help me out of this.

A better way to control what attributes you want to render in your JSON responses, is to use a serializer like for example active_model_serializers
A good article to read about it can be found here SERVING CUSTOM JSON
I wouldn't recommend overwriting default active-record attributes method

Related

How to modify an attributes of an ActiveRecord instance before render it

In my Rails project, I have a model class Employee < ActiveRecord::Base, the class has an attribute called compensation, it is a :jsonb data type in Postgresql.
create_table "employee" do |t|
...
t.jsonb "compensation"
...
end
The compensation contains salary, working_hours, start_from, and etc
compensation : {
salary: 50000,
working_hours: 230,
start_from: 2018-12-21,
...
}
What I wanna do is that before an instance of Employee being rendered as JSON and response to the front-end, I need to remove the salary attribute in the compensation. In employee_controller I tried to use
def get_without_salary
employee = Employee.find 2
employee.compensation.delete :salary
jsonapi_render json: employee
end
but the result JSON still contains the salary data.
I can only make it works by:
temp_compensation = employee.compensation.dup
temp_compensation.delete :salary
employee.compensation = temp_compensation
but it is too ugly and confused me why the first way failed.
Can someone explain to me why? Thanks
This seems a rather tricky question. The options for as_json are the following:
includes: => used to include has_many and belongs_to associations
methods: => to include the result of a class method of the model
except: => prevent columns from showig
only: => only show specified columns
I tried some of them to specify only some attributes of the jsonb column but they either dont work or throw a NoMethodError: undefined method 'serializable_hash' for #<Hash:0x00000005082eb0> showing that it tries to serialize your jsonb but fails.
I think a clean way of doing it may be to have a method filtered_compensation in your model where you define the json you want to send and prevent compensation from showing.
def get_without_salary
employee = Employee.find 2
jsonapi_render json: employee.as_json(methods: [:filtered_compensation], except: [:compensation])
end
Where filtered_compensation is declared in your model
def filtered_compensation
compensation.except 'salary'
end
I think it complies with being clean, separating concerns and makes your method filtered_compensation to be used elsewhere.
Note that instead of having in your response the attribute compensation, you would find filtered_compensation as that is the name of the method.
After a few days thinking, I came up the solution that first converts the active_record instance into a hash, then delete the salary in the hash, and then converts the hash into an active_record instance. This is a better solution comparing to mutating an object on the fly.
def get_without_salary
employee = Employee.find 2
filtered_employee = filter_salary employee
jsonapi_render json: filtered_employee
end
def filter_salary(employee)
record_hash = employee.as_json.deep_symbolize_keys!
record_hash[:compensation]&.delete :salary
Employee.new(record_hash)
end

Adding fields to a Rails model without saving them in the database in Rails

I am rendering a view based on a JSON object representing a Rails model. This JSON object needs fields that are not part of the Rails model, so I'm adding them in the controller as follows in Example 1:
#Controller
events = Event.where(<query>).each do |event|
event[:duration] = (event.end - event.start)/3600
event[:datatime] = event.start.hour + event.start.min/60.0
...
end
render json: events
This renders my data correctly. However, I get the following deprecation warning:
DEPRECATION WARNING: You're trying to create an attribute `duration'. Writing arbitrary attributes on a model is deprecated. Please just use `attr_writer` etc.
I want to rewrite my code to avoid this warning. If I try treating these extra fields as a standard object attribute, the values are not rendered correctly. Below is my attempt to change to using standard object attributes:
#Controller
events = Event.where(<query>).each do |event|
event.duration = (event.end - event.start)/3600
event.datatime = event.start.hour + event.start.min/60.0
...
end
render json: events
#Model
class Event < ActiveRecord::Base
include ActiveModel::AttributeMethods
attr_accessor :duration, :datatime
This causes the fields to be filled with undefined. This is true if I use attr_writer instead of attr_accessor. How can I fix this problem? Am I forced to store these temporary attributes in the database or am I just using the wrong syntax?
If you want to read and write the accessors use attr_accessor:
class Event < ActiveRecord::Base
attr_accessor :duration, :datatime
end
You do not need to include ActiveModel::AttributeMethods to utilize attr_accessor.
To include the accessors in the JSON tell render to include them:
render json: events, methods: [:duration, :datatime]

Rails 4 Not Updating Nested Attributes Via JSON

I've scoured related questions and still have a problem updating nested attributes in rails 4 through JSON returned from my AngularJS front-end.
Question: The code below outlines JSON passed from AngularJS to the Candidate model in my Rails4 app. The Candidate model has many Works, and I'm trying to update the Works model through the Candidate model. For some reason the Works model fails to update, and I'm hoping someone can point out what I'm missing. Thanks for your help.
Here's the json in the AngularJS front-end for the candidate:
{"id"=>"13", "nickname"=>"New Candidate", "works_attributes"=>[
{"title"=>"Financial Analyst", "description"=>"I did things"},
{"title"=>"Accountant", "description"=>"I did more things"}]}
Rails then translates this JSON into the following by adding the candidate header, but does not include the nested attributes under the candidate header and fails to update the works_attributes through the candidate model:
{"id"=>"13", "nickname"=>"New Candidate", "works_attributes"=>[
{"title"=>"Financial Analyst", "description"=>"I did things"},
{"title"=>"Accountant", "description"=>"I did more things"}],
"candidate"=>{"id"=>"13", "nickname"=>"New Candidate"}}
The candidate_controller.rb contains a simple update:
class CandidatesController < ApplicationController
before_filter :authenticate_user!
respond_to :json
def update
respond_with Candidate.update(params[:id], candidate_params)
end
private
def candidate_params
params.require(:candidate).permit(:nickname,
works_attributes: [:id, :title, :description])
end
end
The candidate.rb model includes the following code defining the has_many relationship with the works model:
class Candidate < ActiveRecord::Base
## Model Relationships
belongs_to :users
has_many :works, :dependent => :destroy
## Nested model attributes
accepts_nested_attributes_for :works, allow_destroy: true
## Validations
validates_presence_of :nickname
validates_uniqueness_of :user_id
end
And finally, the works.rb model defines the other side of the has_many relationship:
class Work < ActiveRecord::Base
belongs_to :candidate
end
I appreciate any help you may be able to provide as I'm sure that I'm missing something rather simple.
Thanks!
I've also been working with a JSON API between Rails and AngularJS. I used the same solution as RTPnomad, but found a way to not have to hardcode the include attributes:
class CandidatesController < ApplicationController
respond_to :json
nested_attributes_names = Candidate.nested_attributes_options.keys.map do |key|
key.to_s.concat('_attributes').to_sym
end
wrap_parameters include: Candidate.attribute_names + nested_attributes_names,
format: :json
# ...
end
Refer to this issue in Rails to see if/when they fix this problem.
Update 10/17
Pending a PR merge here: rails/rails#19254.
I figured out one way to resolve my issue based on the rails documentation at: http://edgeapi.rubyonrails.org/classes/ActionController/ParamsWrapper.html
Basically, Rails ParamsWrapper is enabled by default to wrap JSON from the front-end with a root element for consumption in Rails since AngularJS does not return data in a root wrapped element. The above documentation contains the following:
"On ActiveRecord models with no :include or :exclude option set, it will only wrap the parameters returned by the class method attribute_names."
Which means that I must explicitly include nested attributes with the following statement to ensure Rails includes all of the elements:
class CandidatesController < ApplicationController
before_filter :authenticate_user!
respond_to :json
wrap_parameters include: [:id, :nickname, :works_attributes]
...
Please add another answer to this question if there is a better way to pass JSON data between AngularJS and Rails
You can also monkey patch parameter wrapping to always include nested_attributes by putting this into eg wrap_parameters.rb initializer:
module ActionController
module ParamsWrapper
Options.class_eval do
def include
return super if #include_set
m = model
synchronize do
return super if #include_set
#include_set = true
unless super || exclude
if m.respond_to?(:attribute_names) && m.attribute_names.any?
self.include = m.attribute_names + nested_attributes_names_array_of(m)
end
end
end
end
private
# added method. by default code was equivalent to this equaling to []
def nested_attributes_names_array_of model
model.nested_attributes_options.keys.map { |nested_attribute_name|
nested_attribute_name.to_s + '_attributes'
}
end
end
end
end

Model blank due to carrierwave gem - nested_attributes

I've just started to learn Ruby and Ruby on Rails, and this is actually the first time I really have to ask a question on SO, is really making me mad.
I'm programming a REST api where I need to receive an image url and store it in my db.
For this, I've done a model called ImageSet that uses carrierwave to store the uploaded images like this:
class ImageSet < ActiveRecord::Base
has_one :template
mount_uploader :icon1, Icon1Uploader
mount_uploader :icon2, Icon2Uploader
def icon1=(url)
super(url)
self.remote_icon1_url = url
end
def icon2=(url)
super(url)
self.remote_icon2_url = url
end
end
This icon1 and icon2 are both received as urls, hence the setter override, and they can't be null.
My uploader classes are creating some versions with a whitelist of extensions and a override of full_name.
Then, I have this template class that receives nested attributes for ImageSet.
class Template < ActiveRecord::Base
belongs_to :image_set
accepts_nested_attributes_for :image_set
(other stuff)
def image_set
super || build_image_set
end
end
This model has a image_set_id that can't be null.
Considering a simple request, like a post with a json:
{
"template":
{
"image_set_attributes":
{
"icon1": "http....",
"icon2": "http...."
}
}
}
It gives always : ImageSet can't be blank.
I can access temp.image_set from the console,if temp is a Template, and I can set values there too, like, temp.image_set.icon = 'http...' but I can't seem to figure out why is it breaking there.
It should create the image_set, set its attributes a save it for the template class, which would assign its id to the respective column in its own model-
My controller is doing:
(...)
def create
#template = Template.create(params)
if #template
render status: 200
else
render status: 422
end
end
private
def params
params.require(:template).permit(image_set_attributes: [:id, :icon1, :icon2])
end
(...)
Hope you can give me tip on this one.
Thanks!
accepts_nested_attributes doesn't work as expected with belongs_to.
Don't use accepts_nested_attributes_for with belongs_to
Does accepts_nested_attributes_for work with belongs_to?
It can be tricked into working in certain circumstances, but you're better off changing your application elsewhere to do things the "rails way."
Templates validate_presence_of :image_set, right? If so, the problem is that this means ImageSets must always be created before their Templates, but accepts_nested_attributes thinks Template is the parent, and is trying to save the parent first.
The simplest thing you can do is switch the relationship, so Template has_one :image_set, and ImageSet belongs_to :template. Otherwise you're going to have to write some rather odd controller logic to deal with the params as expected.

What is the proper way to access virtual atributes in Ruby on Rails?

I have a model w/ a virtual attribute:
class Campaign < ActiveRecord::Base
def status
if deactivated
return "paused"
else
return "live"
end
end
end
now, in my view, when I access the attribute with campaign.status, I am getting the proper result. However, when I try to access it like this campaign[:status], I get nothing back.
Why is that?
[:status] uses the [] method in Ruby. 'def status' defines a method which shouldn't be mistaken with an ActiveRecord attribute or an virtual attribute (e.g. attr_reader or attr_accessor).
ActiveRecord adds the [] method to your class and makes all the (database) attributes accessible by object[:attr_name] AND object.attr_name(And even object.attributes[:attr_name]).
This is different from how f.e. Javascript works where obj[:method] is virtually the same as obj.method.
Edit: You should be able to use the attr_accessor if you use them for example in any form:
<%= form.input :status %>
Submitting the form will then set the instance variable #status. If you want to do anything with this before or after saving you can call an before_save or after_save hook:
class Campaign < ActiveRecord::Base
attr_accessible :status
attr_accessor :status
before_save :raise_status
def raise_status
raise #status
end
end
This will throw an error with the value submitted value for status.
Hope this helps.

Resources