Difference in rails 4 and rails 5 same code acting differently - ruby-on-rails

I have this class in my rails 4, was working perfectly fine :
class Rule < ActiveModelSerializers::Model
# Required by ActiveModelSerializers in order to seralize this object
# #return [Hash] with all the attributes accessible for serialization
def attributes
...some attributes...
end
def initialize(args = {})
super
#some_custom_variable = ...something...
end
# This method is relevant
def assign_attributes(args)
args.each { |k, v| instance_variable_set("##{k}", v) }
some_custom_variable.assign_attributes(args)
end
end
This class is called from the controller create action like so:
rule = Rule.new(permitted_params.symbolize_keys)
rule.save
The problem I have is that assign_attributes is called when executing controller code (I know this because I've put breakpoint inside), this is just happening in rails 5.
So same code, same params in the controller (generated by same tests) behaves differently. And with rails 5, the code never ends up in assign_attributes method.
My question is why does it behave so with rails 5? Why does assign_attributes gets triggered?

In Rails 4, the assign_attributes method was part of ActiveRecord, in Rails 5 it is part of ActiveModel. This change is mentioned in the Rails5 CHANGELOG which everyone reads prior to upgrading Rails.
Presumably, ActiveModelSerializers::Model includes an assign_attributes method via something in ActiveModel and something in ActiveModel thinks it is calling AM's assign_attributes rather than your accidental override.
However, the project is in a state of chaos (to be generous) so it is difficult to trace through the source:
Changes to 0.10.x maintenance:
The 0.10.x version has become a huge maintenance version. We had hoped to get it in shape for a 1.0 release, but it is clear that isn't
going to happen. Almost none of the maintainers from 0.8, 0.9, or
earlier 0.10 are still working on AMS. We'll continue to maintain
0.10.x on the 0-10-stable branch, but maintainers won't otherwise be actively developing on it.
We may choose to make a 0.11.x (
0-11-stable) release based on 0-10-stable that just removes the
deprecations.
What's happening to AMS:
There's been a lot of churn around AMS since it began back in Rails 3.2 and a lot of new libraries are around and the JSON:API spec has reached 1.0.
If there is to be a 1.0 release of AMS, it will need to address the general needs of serialization in much the way ActiveJob can be used
with different workers.
The next major release is in development. We're starting simple and avoiding, at least at the outset, all the complications in AMS
version, especially all the implicit behavior from guessing the
serializer, to the association's serializer, to the serialization
type, etc.
...
That was from six months ago and there is no useful code available on GitHub anymore, the project looks abandoned to me.
A short term fix would be to rename your assign_attributes method. A long term fix would be to replace ActiveModelSerializers altogether, preferable with something that is being actively maintained. The AMS README even offers some alternatives:
jsonapi-rb is a highly performant and modular JSON:API-only implementation. There's a vibrant community around it that has produced projects such as JSON:API Suite.
fast_jsonapi is a lightning fast JSON:API serializer for Ruby Objects from the team of Netflix.

According to both the Rails 4 and Rails 5 docs, there's nothing about whether Rails itself will use assign_attributes. Just that it's a method you can use to assign a bunch of attributes at once.
Rails 4
Allows you to set all the attributes by passing in a hash of attributes with keys matching the attribute names (which again matches the column names).
Rails 5
Allows you to set all the attributes by passing in a hash of attributes with keys matching the attribute names.
Rule.new calling or not calling Rule#assign_attributes is a quirk of the implementation. It's not documented that new will call assign_attributes so you cannot rely on when Rails will call it.
In addition, if you've overriden assign_attributes it's important that it continue to do what it's documented to do. There should be no functional difference between setting each attribute individually and setting them all at once. It appears rather than call super you've written your own mass assignment code, and added your own extra code to mirror the attributes to another object.
There's also the question of how ActiveModelSerializers::Model might be interfering.
If you want to take action when a particular argument is set, instead override the associated attribute= method.
def foo=(value)
...
end
Or use the various ActiveRecord::Callbacks such as before_validation.
Or since it appears you're trying to delegate the mass setting attributes, consider whether delegation is the answer instead.
delegate :this, :that, to: :some_custom_variable

Related

Rails - how to protect an attribute against updating from outside of the model

In Rails 5 I have a model with an attribute, value of which is never set directly but is always calculated in before_save callback instead. I would like to protect this attribute from being updated from outside of the model, so that calls like f. e. update_attribute() would fail. I used attr_readonly inside the model to achieve what I want and it worked great until I realised that it prevents all updates! Also from within the model itself. Since, according to Rails API docs this is the correct behaviour, what would be the best way to reject modifications to a particular attribute but only from the outside?
You could override the setter. On the model:
def protected_attr_name=(val)
# raise SomeException
end
This disables:
model.protected_attr_name = "value" # => raise exception
model.update_attributes(protected_attr_name: "value") # => raise exception
And then in your before_save method/block:
write_attribute(:protected_attr_name, calculated_value)
Additional observations
Like attr_readonly, you could choose to not raise an exception, and not do anything instead. This may be confusing/frustrating to others working on the same codebase, and is potentially very non-obvious behaviour.
Also - if it is always calculated in the before_save, consider whether this protection is necessary, or whether clearer attribute naming can effectively make the issue go away.

Versioning for Rails i18n translations

I'm in the process of building a volunteer based translation engine for a new site built in Rails 4.0. Since it's volunteer based, there is always the possibility that a user may enter a translation that others do not agree with, accidentally remove a translation, etc. In such an event, I would like to give users the option to revert to a previous translation.
I did some searching around but have yet to find a solution aside from writing my own I18n backend. Is there a simpler way of storing previous versions of translations?
I'm currently using Sven Fuchs' Active Record as a backend, however I'm seriously thinking about switching due to possible performance issues later on down the road.
We had a very successful experience using Globalize (github page: https://github.com/globalize/globalize) and as for the versioning part we didn't try it but Globalize does have support for that in a seperate gem github page: (https://github.com/globalize/globalize-versioning)
After tens of painful gem experiences, i found that comparing gems by last update date and how frequent is new releases, bugs fixes and support is a major factor to decide which one will make your life easier and which one won't.
Update:
You can use Globalize to dynamically translate views (check tutorial) but i came across a github project called iye. I think it suits your needs best (github page: https://github.com/firmafon/iye)
I used Nimir's help to find this solution. Like Globalize Versioning, you can add Paper Trail support for the Active Record's Translation class, however there are a few pitfalls to this method currently.
First you need to include the gems in your Gemfile:
gem "i18n-active_record"
gem "paper_trail"
Then you need to make sure your Translation model class is inheriting from I18n Active Record::Translation and call and calls has_paper_trail:
class Translation < I18n::Backend::ActiveRecord::Translation
has_paper_trail
end
This should really be enough, however the store_translations method in I18n Active Record do not update existing records. Instead, each time a record is added, all records with the given key are deleted and a new record is created. This causes confusion for Paper Trail since it relied on an id.
To get around this issue, I created my own store_translation method, which will update records if they exist:
def store_translations(locale, data, options = {})
escape = options.fetch(:escape, true)
I18n.backend.flatten_translations(locale, data, escape, false).each do |key, value|
t = Translation.find_or_create_by!(locale: locale.to_s, key: key.to_s)
t.value = value
t.save
end
I18n.backend.reload!
end
Notice I also included I18n.backend.reload!, this is because I am running Memoize to cache translations but it seems that it needs to be told to recache whenever a record is updated.
Now I can simply call:
store_translations(lang, {key => key}, :escape => false)
to a store a new translation and ensure we keep a record of the old one and who made the change.

Using ActiveRecord interface for Models backed by external API in Ruby on Rails

I'm trying to use Models in my Rails application that retrieve information from an external API. What I would like to do is access my data models (which may consist of information resulting from multiple API calls) in a way similar to what an ActiveRecord model would provide (specifically associations, and the same style of chain-able query methods).
My initial instinct was to recreate the parts of ActiveRecord that I wanted and incorporate this API. Not wanting to 'reinvent the wheel' and seeing exactly how much work would be required to add more functionality have made me take a step back and reevaluate how to approach this.
I have found ways to use ActiveRecord without a table (see: Railscast #193 Tableless Model and the blog post here) and looked into ActiveRecord. Because ActiveModel only seems to include Validations I'm not sure that's very helpful in this situation. The workaround to using ActiveRecord without a table seems like the best option, but I suspect there's a cleaner way of doing this that I'm just not seeing.
Here is a gist containing some of the code written when I was trying to recreate the ActiveRecord functionality, borrowing heavily from the ActiveRecord source itself.
My question boils down to: I can get the functionality I want (chaining query methods, relations) by either implementing the workaround to ActiveRecord specified above or recreating the functionality myself, but are these really ideal solutions?
Remember that Rails is still just Ruby underneath.
You could represent the external API as instantiated classes within your application.
class Event
def self.find(id)
#...External http call to get some JSON...#
new(json_from_api)
end
def initialize(json)
#...set up your object here...#
end
def attendees
#...external http call to get some JSON and then assemble it
#...into an array of other objects
end
end
So you end up writing local abstractions to create ruby objects from api calls, you can probably also mix in ActiveModel, or Virtus into it, so you can use hash assignment of attributes, and validations for forms etc.
Take a look at an API abstraction I did for the TfL feed for the tube. service_disruption

Is it possible to transparently implement the Future pattern for ActiveRecord queries in Ruby 1.9?

I'm working on an existing Rails 2 site with a large codebase that recently updated to Ruby 1.9.2 and the mysql2 gem. I've noticed that this setup allows for non-blocking database queries; you can do client.query(sql, :async => true) and then later call client.async_result, which blocks until the query completes.
It seems to me that we could get a performance boost by having all ActiveRecord queries that return a collection decline to block until a method is called on the collection. e.g.
#widgets = Widget.find(:all, :conditions=> conditions) #sends the query
do_some_stuff_that_doesn't_require_widgets
#widgets.each do #if the query hasn't completed yet, wait until it does, then populate #widgets with the result. Iterate through #widgets
...
This could be done by monkey-patching Base::find and its related methods to create a new database client, send the query asynchronously, and then immediately return a Delegator or other proxy object that will, when any method is called on it, call client.async_result, instantiate the result using ActiveRecord, and delegate the method to that. ActiveRecord association proxy objects already work similarly to implement ORM.
I can't find anybody who's done this, though, and it doesn't seem to be an option in any version of Rails. I've tried implementing it myself and it works in console (as long as I append ; 1 to the line calling everything so that to_s doesn't get called on the result). But it seems to be colliding with all sorts of other magic and creating various problems.
So, is this a bad idea for some reason I haven't thought of? If not, why isn't it the way ActiveRecord already works? Is there a clean way to make it happen?
I suspect that that .async_result method isn't available for all database drivers; if not, it's not something that could be merged into generic ActiveRecord calls.
A more portable way to help performance when looping over a large recordset would be to use find_each or find_in_batches. I think they'll work in rails 2.3 as well as rails 3.x. http://guides.rubyonrails.org/active_record_querying.html#retrieving-multiple-objects-in-batches

Is there a way to set attr_accessible by default in a rails project with Mongoid?

The recommended solution is this:
config.active_record.whitelist_attributes = true
But this only works if you are using active record. On a mongoid rails project is there a similar approach? It uses active model but not active record.
I've never used Mongoid, so this is pretty speculative, but from the looks of it, AR uses a Railtie initializer to set attr_accessible(nil) when that config is true.
It doesn't look like there's currently way to do that in a config, but you could probably hook it somehow with your own initializer. In Mongoid::Fields, if the config for protect_sensitive_fields is true (the default), it calls attr_protected for id, _id, and _type. That also sets the active_authorizer to a blacklist. You could probably patch that up and give a better config for white list that calls attr_accessible(nil) instead.
So yeah, wouldn't be a bad idea to just make a patch then submit a pull request. The last thing the ruby community needs is another high profile mass assignment fiasco.
I have asked the same question
https://groups.google.com/forum/?fromgroups#!topic/mongoid/xuBbuyhiFEU
It is currently not supported but you can do a (straight forward) monkey patch (as Benedikt
suggested)
https://gist.github.com/1977438
It is very similar to AR (you could check in AR code, I copy it here for simplicity)
ActiveSupport.on_load(:active_record) do
if app.config.active_record.delete(:whitelist_attributes)
attr_accessible(nil)
end
app.config.active_record.each do |k,v|
send "#{k}=", v
end
end
This will do the trick for now:
http://groups.google.com/group/mongoid/browse_thread/thread/de5a93a350b49c02?pli=1

Resources