I've set up an ActiveModel class in my Rails app like this:
class MyThingy
extend ActiveModel::Naming
extend ActiveModel::Translation
include ActiveModel::Validations
include ActiveModel::Conversion
attr_accessor :username, :favorite_color, :stuff
def initialize(params)
#Set up stuff
end
end
I really want to be able to do this:
thingy = MyThingy.new(params)
thingy.update_attributes(:favorite_color => :red, :stuff => 'other stuff')
I could just write update_attributes on my own, but I have a feeling it exists somewhere. Does it?
No, but there's common pattern for this case:
class Customer
include ActiveModel::MassAssignmentSecurity
attr_accessor :name, :credit_rating
attr_accessible :name
attr_accessible :name, :credit_rating, :as => :admin
def assign_attributes(values, options = {})
sanitize_for_mass_assignment(values, options[:as]).each do |k, v|
send("#{k}=", v)
end
end
end
It's from here. See the link for examples.
If you find yourself repeating this approach often, you can extract this method into a separate module and include include it on demand.
It looks like they pulled it out of active record and moved it to active model in Rails 5.
http://api.rubyonrails.org/classes/ActiveModel/AttributeAssignment.html#method-i-assign_attributes
You should be able to include the module:
include ActiveModel::AttributeAssignment
Related
class Api::V1::BookSerializer < ActiveModel::Serializer
attributes :id, :status, :name, :author_name, :published_date
attributes :conditional_attributes if condition_1?
belongs_to :user if condition_2?
end
Here I want to put condition on action basic of the controller.
For example I will like to send conditional_attributes for only index action and not for other actions.
But rails "active_model_serializers", "~> 0.10.0" does not give any such things according to my knowledge.
Something like this should do the trick:
class Api::V1::BookSerializer < ActiveModel::Serializer
attributes :id, :status, :name, :author_name, :published_date
attribute :conditional_attribute, if: :some_condition?
belongs_to :conditional_association, if: :some_other_condition?
private
def some_condition?
# some condition
end
def some_other_condition?
# some other condition
end
end
You can also use :unless for negated conditions.
You can use instance_options or instance_reflections in your conditions if you need them (see https://github.com/rails-api/active_model_serializers/blob/0-10-stable/docs/howto/passing_arbitrary_options.md) or you can use scopes (see https://github.com/rails-api/active_model_serializers/blob/0-10-stable/docs/general/serializers.md#scope)
Note: To the best of my knowledge, this only works with attribute and association methods – it doesn't work with attributes (see https://github.com/rails-api/active_model_serializers/blob/0-10-stable/lib/active_model/serializer.rb#L204-L210) since it doesn't pass options along.
I read your comment regarding sticking with AM Serializers, but I'll still point it out: If you're looking for a more robust and flexible solution than AM Serializers, jsonapi-serializer or Blueprinter work quite well and both have support for conditional fields as well as conditional associations.
I assume you're trying to render from the controller.
You can pass options to your serializer from the call to render:
render json: #track, serializer: Api::V1::BookSerializer, return_user: return_user?, return_extra_attributes: return_extra_attributes?
You can then access that option in your serializer definition, via #instance_options[:your_option].
Here, you would likely have something like:
class Api::V1::BookSerializer < ActiveModel::Serializer
attributes :id, :status, :name, :author_name, :published_date
attributes :conditional_attributes if return_conditional_attributes?
belongs_to :user if return_user?
def return_conditional_attributes?
#instance_options[:return_extra_attributes]
end
def return_user?
#instance_options[:return_user]
end
end
return_extra_attributes? and return_extra_attributes? would be method defined in your controller
documentation here: https://github.com/rails-api/active_model_serializers/blob/0-10-stable/docs/howto/passing_arbitrary_options.md
I have used AMS (0.8) with Rails 3.2.19 but one place where I really struggle with them is how to control whether serializers include their associations or not. I obviously use AMS to build JSON
Api's. Sometimes a serializer is the leaf or furthest out element and sometimes it's the top level and needs to include associations. My question is what is the best way to do this or is the solution I do below work (or is best solution).
I have seen some of the discussions but I find them very confusing (and version based). It's clear that for Serializer attributes or associations, there is an an include_XXX? method for each and you can return either a truthy or falsey statement here.
Here's my proposed code - it's a winemaker that has many wine_items. Is this how you would do this?
Model Classes:
class WineItem < ActiveRecord::Base
attr_accessible :name, :winemaker_id
belongs_to :winemaker
end
class Winemaker < ActiveRecord::Base
attr_accessible :name
has_many :wine_items
attr_accessor :show_items
end
Serializers:
class WinemakerSerializer < ActiveModel::Serializer
attributes :id, :name
has_many :wine_items
def include_wine_items?
object.show_items
end
end
class WineItemSerializer < ActiveModel::Serializer
attributes :id, :name
end
and in my controller:
class ApiWinemakersController < ApplicationController
def index
#winemakers=Winemaker.all
#winemakers.each { |wm| wm.show_items=true }
render json: #winemakers, each_serializer: WinemakerSerializer, root: "data"
end
end
I ran into this issue myself and this is the cleanest solution so far (but I'm not a fan of it).
This method allows you to do things like:
/parents/1?include_children=true
or using a cleaner syntax like:
/parents/1?include=[children], etc...
# app/controllers/application_controller.rb
class ApplicationController
# Override scope for ActiveModel-Serializer (method defined below)
# See: https://github.com/rails-api/active_model_serializers/tree/0-8-stable#customizing-scope
serialization_scope(:serializer_scope)
private
# Whatever is in this method is accessible in the serializer classes.
# Pass in params for conditional includes.
def serializer_scope
OpenStruct.new(params: params, current_user: current_user)
end
end
# app/serializers/parent_serializer.rb
class ParentSerializer < ActiveModel::Serializer
has_many :children
def include_children?
params[:include_children] == true
# or if using other syntax:
# params[:includes].include?("children")
end
end
Kinda hackish to me, but it works. Hope you find it useful!
We have custom model. It is working without database and includes some mixins from active record:
class Node
include ActiveModel::Validations
include ActiveModel::Conversion
extend ActiveModel::Naming
attr_accessor :title, :content
validates_presence_of :title, :content
def initialize(attributes = {})
attributes.each do |name, value|
send("#{name}=", value)
end
end
def persisted?
false
end
def save
# we want to run validations here
end
end
Through googling got that it is possible to use #object.validate, but it complains about not having such method.
Help, please.
You are correct, .validate seems to be undefined.
#object.valid? should do the job for what you want.
Keep in mind, this returns a boolean value which you can use to control conditional behaviour based on your requirements.
So I have a few different models in my Rails 4 app that have image uploads. Rather than adding identical code to each of the models I've created a module that I can include into all of them.
Here it is:
module WithImage
extend ActiveSupport::Concern
included do
attr_accessor :photo
has_one :medium, as: :imageable
after_save :find_or_create_medium, if: :photo?
def photo?
self.photo.present?
end
def find_or_create_medium
medium = Medium.find_or_initialize_by_imageable_id_and_imageable_type(self.id, self.class.to_s)
medium.attachment = photo
medium.save
end
end
def photo_url
medium.attachment if medium.present?
end
end
class ActiveRecord::Base
include WithImage
end
A Medium (singular of media) in this case is a polymorphic model that has paperclip on it. The attr_accessor is a f.file_field :photo that I have on the various forms.
Here's my PurchaseType Model (that uses this mixin):
class PurchaseType < ActiveRecord::Base
include WithImage
validates_presence_of :name, :type, :price
end
So here's the thing, the after_save works great here. However, when I go to the console and do PurchaseType.last.photo_url I get the following error:
ActiveRecord::ActiveRecordError: ActiveRecord::Base doesn't belong in a hierarchy descending from ActiveRecord
I haven't the faintest clue what this means or why it is happening. Anyone have any insight?
Thanks!
It turns out I was trying to do things I had seen in various examples of modules. It was simple to get it working:
module WithImage
extend ActiveSupport::Concern
included do
attr_accessor :photo
has_one :medium, as: :imageable
after_save :find_or_create_medium, if: :photo?
def photo?
self.photo.present?
end
def find_or_create_medium
medium = Medium.find_or_initialize_by_imageable_id_and_imageable_type(self.id, self.class.to_s)
medium.attachment = photo
medium.save
end
def photo_url
medium.attachment.url if medium.present?
end
end
end
I have the following mogoid document definition/class:
class Exercise
include Mongoid::Document
field :name, :type => String
field :description, :type => String
belongs_to :group
validates_presence_of :name, :description
end
I then have the following controller and save method:
class ExercisesController < ApplicationController
respond_to :json
def create
#exercise = Exercise.create(params[:exercise])
#exercise.save!
respond_with #exercise
end
end
This seems wrong to me and open to mass assignment problems.
How do people normally protect against this and would using the strong parameters gem be a good idea?
Yes you should use the strong_parameters gem, it will be the default mass-assignment protection in rails 4
You can use attr_accessible as 'standard' protection. This of course still has the disadvantage that you expose a lot of fields to the interface, whereas you might want to expose only a few, but need to expose those fields in other controllers.