I'm trying to use a postgreSQL column of type hstore array and everything seems to work just fine. However, my views escapes the array and turns it to bad formatted string. My code looks like that:
Migration:
class AddVariantsToItem < ActiveRecord::Migration
def change
execute 'CREATE EXTENSION hstore'
add_column :items, :variants, :hstore, array: true, default: []
end
end
And now, if i will, for instance, use Item.last.variants in rails console, it gives me a proper array
> Item.last.variants
#=> [{"name"=>"Small", "price"=>"12.00"}, {"name"=>"Medium", "price"=>"20.00"}]
However, using the exact same code in slim views gives me a escaped string:
div
= debug Item.last.variants
/ Gives me:
/ '{"\"name\"=>\"Small\", \"price\"=>\"12.00\"","\"name\"=>\"Medium\", \"price\"=>\"20.00\""}'
Using raw, == or .html_save does not changes anything. Can anyone tell me if i can do anything about it?
Item.last.variants is an Array. When putting something into view, its being stringified (I'm not sure, but I think its to_s method or inspect).
My advice is that you shouldn't put whole objects. In this particular example, I think you should iterate over it and show data manually.
Related
I am currently upgrading a Ruby on Rails app from 4.2 to 5.0 and am running into a roadblock concerning fields that store data as a serialized hash. For instance, I have
class Club
serialize :social_media, Hash
end
When creating new clubs and inputting the social media everything works fine, but for the existing social media data I'm getting:
ActiveRecord::SerializationTypeMismatch: Attribute was supposed to be a Hash, but was a ActionController::Parameters.
How can I convert all of the existing data from ActionController::Parameter objects to simple hashes? Database is mysql.
From the fine manual:
serialize(attr_name, class_name_or_coder = Object)
[...] If class_name is specified, the serialized object must be of that class on assignment and retrieval. Otherwise SerializationTypeMismatch will be raised.
So when you say this:
serialize :social_media, Hash
ActiveRecord will require the unserialized social_media to be a Hash. However, as noted by vnbrs, ActionController::Parameters no longer subclasses Hash like it used to and you have a table full of serialized ActionController::Parameters instances. If you look at the raw YAML data in your social_media column, you'll see a bunch of strings like:
--- !ruby/object:ActionController::Parameters...
rather than Hashes like this:
---\n:key: value...
You should fix up all your existing data to have YAMLized Hashes in social_media rather than ActionController::Parameters and whatever else is in there. This process will be somewhat unpleasant:
Pull each social_media out of the table as a string.
Unpack that YAML string into a Ruby object: obj = YAML.load(str).
Convert that object to a Hash: h = obj.to_unsafe_h.
Write that Hash back to a YAML string: str = h.to_yaml.
Put that string back into the database to replace the old one from (1).
Note the to_unsafe_h call in (3). Just calling to_h (or to_hash for that matter) on an ActionController::Parameters instance will give you an exception in Rails5, you have to include a permit call to filter the parameters first:
h = params.to_h # Exception!
h = params.permit(:whatever).to_h # Indifferent access hash with one entry
If you use to_unsafe_h (or to_unsafe_hash) then you get the whole thing in a HashWithIndifferentAccess. Of course, if you really want a plain old Hash then you'd say:
h = obj.to_unsafe_h.to_h
to unwrap the indifferent access wrapper as well. This also assumes that you only have ActionController::Parameters in social_media so you might need to include an obj.respond_to?(:to_unsafe_hash) check to see how you unpack your social_media values.
You could do the above data migration through direct database access in a Rails migration. This could be really cumbersome depending on how nice the low level MySQL interface is. Alternatively, you could create a simplified model class in your migration, something sort of like this:
class YourMigration < ...
class ModelHack < ApplicationRecord
self.table_name = 'clubs'
serialize :social_media
end
def up
ModelHack.all.each do |m|
# Update this to match your real data and what you want `h` to be.
h = m.social_media.to_unsafe_h.to_h
m.social_media = h
m.save!
end
end
def down
raise ActiveRecord::IrreversibleMigration
end
end
You'd want to use find_in_batches or in_batches_of instead all if you have a lot of Clubs of course.
If your MySQL supports json columns and ActiveRecord works with MySQL's json columns (sorry, PostgreSQL guy here), then this might be a good time to change the column to json and run far away from serialize.
Extending on short's reply - a solution that does not require a database migration:
class Serializer
def self.load(value)
obj = YAML.load(value || "{}")
if obj.respond_to?(:to_unsafe_h)
obj.to_unsafe_h
else
obj
end
end
def self.dump(value)
value = if value.respond_to?(:to_unsafe_h)
value.to_unsafe_h
else
value
end
YAML.dump(value)
end
end
serialize :social_media, Serializer
Now club.social_media will work whether it was created on Rails 4 or on Rails 5.
The reply by #schor was a life-saver, but I kept getting no implicit conversion of nil into String errors when doing the YAML.load(value).
What worked for me was:
class Foo < ApplicationRecord
class NewSerializer
def self.load(value)
return {} if !value #### THIS NEW LINE
obj = YAML.load(value)
if obj.respond_to?(:to_unsafe_h)
obj.to_unsafe_h
else
obj
end
end
def self.dump(value)
if value.respond_to?(:to_unsafe_h)
YAML.dump(value.to_unsafe_h)
else
YAML.dump(value)
end
end
end
serialize :some_hash_field, NewSerializer
end
I gotta admin the Rails team totally blindsided me on this one, a most unwelcome breaking change that doesn't even let an app fetch the "old" data.
The official Ruby on Rails documentation has a section about upgrading between Rails versions that explains more about the error you have:
ActionController::Parameters No Longer Inherits from HashWithIndifferentAccess
Calling params in your application will now return an object instead of a hash. If your parameters are already permitted, then you will not need to make any changes. If you are regardless of permitted? you will need to upgrade your application to first permit and then convert to a hash.
params.permit([:proceed_to, :return_to]).to_h
Run a migration on Rails 4 to prepare the data for Rails 5.
We're going through the exact same thing, except we serialize as ActiveSupport::HashWithIndifferentAccess instead of just Hash, which I recommend doing, but I'll provide my answer here for just a simple Hash.
If you have not yet upgraded to Rails 5, which I hope you haven't and your tests have uncovered this issue, you can run a migration on the Rails 4 branch that will get your data ready for Rails 5.
It essentially re-serializes all of your records from ActionController::Parameters to Hash while in Rails 4 and ActionController::Parameters still inherits from HashWithIndifferentAccess.
class ConvertSerializedActionControllerParametersToHashInClubs < ActiveRecord::Migration
disable_ddl_transaction! # This prevents the locking of the table (e.g. in production).
def up
clubs = Club.where.not( social_media: nil )
total_records = clubs.count
say "Updating #{ total_records } records."
clubs.each.with_index( 1 ) do |club, index|
say "Updating #{ index } of #{ total_records }...", true
club.social_media = club.social_media.to_h
club.social_media_will_change!
club.save
end
end
def down
puts "Cannot be reverse! See backup table."
end
end
If you have multiple columns that need to be converted, it's easy to modify this migration to convert all of the necessary tables and columns.
Depending on when you do this, your data should be ready for Rails 5.
I'm trying to send the count on an active Record object's association to the redis-object gem
class Post > ActiveRecord::Base
has_many :comments
include Redis::Objects
value :redis_comment_count, :default => self.comments.count
end
PS: setting :default => "string" works just fine
but this does not work because self at that place in the code (its not in a method definition) refers to the class definition of Post and not a post instance itself. I was trying to figure out if this was something that was even possible to do.
Am I making sense?
Unfortunately, it looks like you will have to go the longer way of setting up save callbacks for your comments. I browsed through the gem and it doesn't look like passing a proc for calling later is supported yet (see here).
By the way:
Besides the fact that you are calling the class method and not the instance method, self.comments.count is evaluated when the class is loaded, right there when you call:
value :redis_comment_count, :default => self.comments.count
# This becomes:
# value :redis_comment_count, :default => 1 # Example
and not every time that the redis-objects gem uses value_options[:default].
This value will keep getting re-evaluated if your class keeps getting reloaded, as in the case of the default setup for the development environment. However, in the production environment where we usually have cache_classes enabled, this value will be evaluated only whenever the Rails application boots up and loads your models.
Passing a proc would work if this were supported.
Have you tried?
value :redis_comment_count, default: -> { self.comments.count}
That's using Ruby 1.9's new hash syntax along with the 'stab' or lambda operator.
I have a class I've extended from ActiveRecord::Base...
class Profile < ActiveRecord::Base
and I collect the records from it like so...
records = #profile.all
which works fine, but it doesn't seem that I can successfully Update the attributes. I don't want to save them back to the database, just modify them before I export them as JSON. My question is, why can't I update these? I'm doing the following (converting date formats before exporting):
records.collect! { |record|
unless record.term_start_date.nil?
record.term_start_date = Date.parse(record.term_start_date.to_s).strftime('%Y,%m,%d')
end
unless record.term_end_date.nil?
record.term_end_date = Date.parse(record.term_end_date.to_s).strftime('%Y,%m,%d')
end
record
}
At first I had just been doing this in a do each loop, but tried collect! to see if it would fix things, but no difference. What am I missing?
P.S. - I tried this in irb on one record and got the same results.
I suggest a different way to solve the problem, that keeps the logic encapsulated in the class itself.
Override the as_json instance method in your Profile class.
def as_json(options={})
attrs = super(options)
unless attrs['term_start_date'].nil?
attrs['term_start_date'] = Date.parse(attrs['term_start_date'].to_s).strftime('%Y,%m,%d')
end
unless attrs['term_end_date'].nil?
attrs['term_end_date'] = Date.parse(attrs['term_end_date'].to_s).strftime('%Y,%m,%d')
end
attrs
end
Now when you render the records to json, they'll automatically use this logic to generate the intermediate hash. You also don't run the risk of accidentally saving the formatted dates to the database.
You can also set up your own custom option name in the case that you don't want the formatting logic.
This blog post explains in more detail.
Try to add record.save! before record.
Actually, by using collect!, you just modifying records array, but to save modified record to database you should use save or save! (which raises exception if saving failed) on every record.
I'm new to RoR/Gems, this is a basic question.
I created a gem, MyNameGem, in order to learn the process. It contains these methods:
def returnValidationString1
puts 'Validation String'
end
def returnValidationString2
puts 'ANother Validation String'
end
I included the gem in a simple rails app, everything seems to be working as expected.
I this to my model:
validates :name => MyNameGem.returnValidationString1
What I'm trying to create is a gem that I can use inside a validation routine. So, for example, I want to do this: validates :name => (call my gem method, return a string, and use that string as the validation requirement)
puts only prints to console.
if you want to return 'MyNameGem' write return 'MyNameGem' or simply 'MyNameGem because the last line gets returned automatically.
The function of puts is to put things to the console, so that's exactly what it will do. What your validates call is doing is kind of unusual though, and doesn't seem to make any sense. Your code evaluates to:
validates :name => (puts "MyNameGem")
That really doesn't mean anything. puts usually returns nil.
If you want to write a custom validation routine that's stored in a gem, that's a different question.
I have an issue here where I'm trying to call a class method on an object that is not known... err, I'm not sure how to phrase this I'm getting the :resource from the URL, but I want to run find on it for a differnt param as well.
How can I do something like:
params[:resource].classify.find(params[:id])
I mean this won't work because the params[:resource].classify would be a string. But how can I run a method on it as if it was a Class and not a string?
The following used to work fine but the gem friendly_id has made all my calls to a record to return it's friendly_id and not its actual primary key... which totally sucks.
It was doing something like this, which worked just fine:
#vote = Vote.new({
:vote => params[:direction] == 'up' ? true : false,
:voteable_type => params[:resource].classify,
:voteable_id => params[:id])
})
But since adding friendly_id my paths now look something like:
/things/my-thing-name/vote/up
instead of the old way:
/things/328/vote/up
So now the params[:id] is no longer the foreign key.
Thoughts?
I'm a bit confused by your question, it seems like 2.
For part 1, you can constantize. params[:resource].classify.constantize should return the classname that you can then invoke a method on. Just to be safe, you might want to tableize before constantizeing, just to make sure things like "-" are going to be "_". I only mention this because of how you have your friendly_id set up.
As for part 2, I don't know the friendly_id gem, but based off of the description of how it works in the guide, your find should still work just fine unless I'm missing something.
For the first thing "params[:resource].classify" you need to do
params[:resource].constantize
For the second it looks like you should do something like:
#thing = Thing.find_by_friendly_id(params[:friendly_id])