I've ported a Rails app from Rails 3 to Rails 4 and most things work now, except for a problem with two levels of nested attributes:
I have ProductGroups, Variants and Prices.
Each ProductGroup has one or more variants. One of them is the master variant.
Each variant has many prices (one for each region).
I have a controller that updates ProductGroups. When the ProductGroup is updated, the master variant is updated at the same time. And prices in the master variant are also updated.
Here's a test that describes what's expected to happend:
test "should update master variant" do
login_as accounts(:johnny_admin)
p = ProductGroup.find product_groups(:toothbrush).id
assert_equal "10123", p.artno
assert_equal "10123", p.master_variant.artno
puts(p.master_variant.prices.to_a.to_s)
post :update,
id: product_groups(:toothbrush),
p: 'setup',
product_group: {
master_variant_attributes: {
artno: "20222",
supplier_artno: "1010",
prices_attributes: { "0": { price: "55", id: prices(:toothbrush_price_se).id } }
}
}
assert_response :redirect
assert_redirected_to edit_admin_product_group_path(p, :p => 'setup')
p = ProductGroup.find product_groups(:toothbrush).id
assert_equal "20222", p.artno
assert_equal "20222", p.master_variant.artno
assert_equal "1010", p.master_variant.supplier_artno
price = Prices.find prices(:toothbrush_price_se).id
assert_equal 55, price.price
end
But it fails with this error:
# Running:
.......[#<Price id: 510149407, variant_id: 630858089, region_id: 102782309, price: #<BigDecimal:55d2732f50a8,'0.95E2',9(18)>, created_at: "2016-12-30 11:14:28", updated_at: "2016-12-30 11:14:28">, #<Price id: 524805804, variant_id: 630858089, region_id: 960235695, price: #<BigDecimal:55d27339c510,'0.1E2',9(18)>, created_at: "2016-12-30 11:14:28", updated_at: "2016-12-30 11:14:28">]
E
Finished in 1.279989s, 6.2501 runs/s, 20.3127 assertions/s.
1) Error:
Admin::ProductGroupsControllerTest#test_should_update_master_variant:
ActiveRecord::RecordNotFound: Couldn't find Price with ID=510149407 for Variant with ID=
app/controllers/admin/product_groups_controller.rb:150:in `update'
test/functional/admin/product_groups_controller_test.rb:103:in `block in <class:ProductGroupsControllerTest>'
As you can see in the debug output, there is a price with ID 510149407 for that variant. And why is the ID of the variant empty?
I'm totally stuck.
Here's the permits for ProductGroup that I'm using:
def product_group_params
prices_attributes = { :prices_attributes => [ :id, :price ] }
master_variant_attributes = { :master_variant_attributes => [
:unit, :vat, :artno, :width, :height, :depth,
:description, :in_stock, :in_stock_verified_at,
:status, :supplier_id, :supplier_artno,
:alt_supplier_id, :alt_supplier_artno,
:supplier_price, :alt_supplier_price,
:supplier_reduction, :alt_supplier_reduction,
:supplier_carriage_percentage, :alt_supplier_carriage_percentage,
:our_expenses, :percentage_markup, :taric_code_id,
:reduction_group_id, :vendor_id, :vendor_artno, :is_expired,
:show_price, :reorder_point,
:place_of_storage_a, :place_of_storage_b, :place_of_storage_c,
prices_attributes
] }
params.require(:product_group).permit(:updated_by,
:title, :description, :license_code, :fixme,
master_variant_attributes,
:notes, :vat, :artno, :unit,
:width, :height, :depth, :in_stock, :published, :reorder_point,
:current_version, :changelog, :price_by_start_cost_and_per_unit,
:start_cost_variant_id, :unit_cost_variant_id,
:category_ids => [])
end
Here's how ProductGroup relates to the master variant:
has_one :master_variant,
-> { where(is_master: true, deleted_at: nil) },
:class_name => "Variant",
:foreign_key => 'product_group_id',
:dependent => :destroy,
:autosave => true
accepts_nested_attributes_for :master_variant
Here's how Variant relates to Prices:
has_many :prices, -> { order('region_id') }, :dependent => :destroy
accepts_nested_attributes_for :prices
I will gladly post any other excerpts from the code if it is of any help, but I'm not sure what could be of interest right now.
Any hints would be much appreciated!
Related
I have models Favorite_Photo, User, and Photo
In Heroku Console:
u = User.find(1)
u.favorites.last
=> #<Photo id: 37, user_id: 1, picture: "th.jpeg", title: "Cookies & Cream Pocky ", description: nil, photo_type: nil, location_type: nil, remote_picture_url: nil, created_at: "2016-07-07 03:04:03", updated_at: "2016-07-07 03:04:03">
And If I query:
u = User.find(1)
u.favorite_photos.last
=> #<FavoritePhoto id: 87, photo_id: 12, user_id: 1, created_at: "2016-07-07 19:37:28", updated_at: "2016-07-07 19:37:28">
class User
has_many :favorite_photos
has_many :favorites, through: :favorite_photos, source: :photo
class Photo
has_many :favorite_photos
has_many :favorited_by, through: :favorite_photos, source: :user
class FavoritePhoto
belongs_to :user
belongs_to :photo
validates :user_id, uniqueness: {
scope: [:photo_id],
message: 'can only favorite an item once'
}
UsersController
def show
#user = User.find(params[:id])
#favorites = #user.favorites
end
This returns a list of favorites ordered by photo_id. I want to create a scope that will order the favorites based on FavoritePhoto id:
has_many :favorites, -> { order("favorite_photos.id ASC") }, through: :favorite_photos, source: :photo
reference: scopes for has_many
I have two models: Cabinet and Workplace.
class Cabinet < ActiveRecord::Base
def as_json(options={})
options.merge!({except: [:created_at, :updated_at]})
super(options)
end
end
class Workplace < ActiveRecord::Base
belongs_to :cabinet
def as_json(options = {})
options.merge!(:except => [:created_at, :updated_at, :cabinet_id], include: :cabinet)
super(options)
end
end
When I called Cabinet.first.to_json I get
{
id: 1,
cabinet: "100"
}
but when I called Workplace.first.to_json id get
{
name: "first workplace",
Cabinet: {
id: 1,
cabinet: "100",
created_at: "#created_at",
updated_at: "#updated_at"
}
}
Why this? Thanks and sorry for my english :)
Not sure if I am following you, but do you want to get just attributes from Workplace model, and not Cabinet data when you do Workplace.first.to_json?
I think it is because you include cabinet in as_json method configuration as explained here.
You should either remove it or do this:
Workplace.first.attributes.to_json
Let me know if I am missing something from your question.
Let's assume that your model Cabinet has :id, :cabinet, :created_at, :updated_at attributes and Workplace has :id, :name, :cabinet_id, .....
Now, if you try to fire Cabinet.first.to_json, ofcourse it will render the following:
{
id: 1,
cabinet: "100"
}
becuase that is the attributes belongs to Cabinet model. Then you also added these line of code options.merge!({except: [:created_at, :updated_at]}) that's why it only renders :id and :name attributes. And if you try to fire Workplace.first.to_json then it will render:
{
name: "first workplace",
Cabinet: {
id: 1,
cabinet: "100",
created_at: "#created_at",
updated_at: "#updated_at"
}
}
because, of these options.merge!(:except => [:created_at, :updated_at, :cabinet_id], include: :cabinet). You include the model Cabinet so it will automatically added to your json.
In my app I had BlogPost model and User model that are related through relation named author. To serve data from my Rails app I use active_model_serializers with definition:
class Blog::PostSerializer < ActiveModel::Serializer
embed :ids, include: true
attributes :id, :title, :text, :created_at, :updated_at
has_one :author
has_many :assets
end
When I fetch this using Ember model:
Admin.BlogPost = DS.Model.extend({
author: DS.belongsTo('User'),
title: DS.attr('string'),
text: DS.attr('string'),
createdAt: DS.attr('date'),
updatedAt: DS.attr('date')
});
There is an error:
Uncaught Error: Assertion Failed: You looked up the 'author' relationship on a 'blog.post' with id 1 but some of the associated records were not loaded. Either make sure they are all loaded together with the parent record, or specify that the relationship is async (`DS.belongsTo({ async: true })`)
Which is caused by that my response looks like:
{
'blog_posts': [
{
id: 1,
author_id: 1
},
// …
],
'authors': [
{ id: 1, /* … */ }
]
}
Is there any way to change 'authors' in response to 'users' or use 'authors' as alias to 'users' in serializer?
From active_model_serializers 0.8 description: https://github.com/rails-api/active_model_serializers/tree/0-8-stable
You can also specify a different root for the embedded objects than the key used to reference them:
class PostSerializer < ActiveModel::Serializer
embed :ids, :include => true
attributes :id, :title, :body
has_many :comments, :key => :comment_ids, :root => :comment_objects
end
This would generate JSON that would look like this:
{"post": {
"id": 1,
"title": "New post",
"body": "A body!",
"comment_ids": [ 1 ]
},
"comment_objects": [
{ "id": 1, "body": "what a dumb post" }
]
}
Just define a method in your serializer named users and return authors in it I.e.
attributes :id, :title, :text, :created_at, :updated_at, :users
def users
object.authors
end
I have a model item has_many ratings and a ratings belongs_to item ratings belongs_to user I want to force a user who is creating an item to rate it too. Other users can then rate it later on. item and user have no association in my model.
I am doing the following in my item_spec which is giving me an error no implicit conversion of Symbol into Integer on line #item = Item.new(name: "Item1", below.
class Item < ActiveRecord::Base
has_many :ratings, dependent: :destroy, inverse_of: :item
accepts_nested_attributes_for :ratings, :allow_destroy => true
validates :name , :length => { minimum: 3 }
validates :category , :length => { minimum: 3 }
validates_presence_of :ratings
end
require 'spec_helper'
describe Item do
before do
#item = Item.new(name: "Item1",
url: "www.item1.com",
full_address: "Item1Address",
city: "Item1City",
country: "Item1Country",
category: "Item1Type",
ratings_attributes: {"rating" => "3", "comment" => "Ahh Good"} )
end
Also using FactoryGirl I am doing something like this
factory :item do
before_create do |r|
r.ratings<< FactoryGirl.build(:ratings, item: r )
end
name "Item1"
url "www.Item1.com"
full_address "Item1Address"
city "Item1City"
country "Item1Country"
category "Item1Category"
end
factory :ratings do
rating 3
comment "Its not that bad"
user
end
end
which again is not yeilding the desired result.
can anyone help me solve this problem please.Thanks!
Working Code, now having problem testing some association order, but at least the desired functionality working.
factory :item do
name "Item1"
url "www.Item1.com"
full_address "Item1Address"
city "Item1City"
country "Item1Country"
category "Item1Category"
end
factory :ratings, :class => 'Ratings' do
association :item, factory: :item, strategy: :build
user
rating 3
comment "Its not that bad"
end
factory :item_with_rating, parent: :item do
ratings {[FactoryGirl.create(:ratings)]}
end
Here is the spec file
require 'spec_helper'
describe Item do
before do
#item = FactoryGirl.create(:item_with_rating)
end
subject { #item }
it { should respond_to(:name) }
it { should respond_to(:url) }
it { should respond_to(:full_address)}
it { should respond_to(:city) }
it { should respond_to(:country) }
it { should respond_to(:category) }
it { should respond_to(:ratings) }
it { should_not respond_to(:type) }
it { should_not respond_to(:user_id) }
it { should be_valid }
There is no change in the Model file for item
What do you think is the most optimal way to retrieve all attributes for all the associations an AR model has?
i.e: let's say we have the model Target.
class Target < ActiveRecord::Base
has_many :countries
has_many :cities
has_many :towns
has_many :colleges
has_many :tags
accepts_nested_attributes_for :countries, :cities, ...
end
I'd like to retrieve all the association's attributes by calling a method on a Target instance:
target.associations_attributes
>> { :countries => { "1" => { :name => "United States", :code => "US", :id => 1 },
"2" => { :name => "Canada", :code => "CA", :id => 2 } },
:cities => { "1" => { :name => "New York", :region_id => 1, :id => 1 } },
:regions => { ... },
:colleges => { ... }, ....
}
Currently I make this work by iterating on each association, and then on each model of the association, But it's kind of expensive, How do you think I can optimize this?
Just a note: I realized you can't call target.countries_attributes on has_many associations with nested_attributes, one_to_one associations allow to call target.country_attributes
I'm not clear on what you mean with iterating on all associations. Are you already using reflections?
Still curious if there's a neater way, but this is what I could come up with, which more or less results in the hash you're showing in your example:
class Target < ActiveRecord::Base
has_many :tags
def associations_attributes
# Get a list of symbols of the association names in this class
association_names = self.class.reflect_on_all_associations.collect { |r| r.name }
# Fetch myself again, but include all associations
me = self.class.find self.id, :include => association_names
# Collect an array of pairs, which we can use to build the hash we want
pairs = association_names.collect do |association_name|
# Get the association object(s)
object_or_array = me.send(association_name)
# Build the single pair for this association
if object_or_array.is_a? Array
# If this is a has_many or the like, use the same array-of-pairs trick
# to build a hash of "id => attributes"
association_pairs = object_or_array.collect { |o| [o.id, o.attributes] }
[association_name, Hash[*association_pairs.flatten(1)]]
else
# has_one, belongs_to, etc.
[association_name, object_or_array.attributes]
end
end
# Build the final hash
Hash[*pairs.flatten(1)]
end
end
And here's an irb session through script/console to show how it works. First, some environment:
>> t = Target.create! :name => 'foobar'
=> #<Target id: 1, name: "foobar">
>> t.tags.create! :name => 'blueish'
=> #<Tag id: 1, name: "blueish", target_id: 1>
>> t.tags.create! :name => 'friendly'
=> #<Tag id: 2, name: "friendly", target_id: 1>
>> t.tags
=> [#<Tag id: 1, name: "blueish", target_id: 1>, #<Tag id: 2, name: "friendly", target_id: 1>]
And here's the output from the new method:
>> t.associations_attributes
=> {:tags=>{1=>{"id"=>1, "name"=>"blueish", "target_id"=>1}, 2=>{"id"=>2, "name"=>"friendly", "target_id"=>1}}}
try this with exception handling:
class Target < ActiveRecord::Base
def associations_attributes
tmp = {}
self.class.reflections.symbolize_keys.keys.each do |key|
begin
data = self.send(key) || {}
if data.is_a?(ActiveRecord::Base)
tmp[key] = data.attributes.symbolize_keys!
else
mapped_data = data.map { |item| item.attributes.symbolize_keys! }
tmp[key] = mapped_data.each_with_index.to_h.invert
end
rescue Exception => e
tmp[key] = e.message
end
end
tmp
end
end
This is updated version of Stéphan Kochen's code for Rails 4.2
def associations_attributes
association_names = self.class.reflect_on_all_associations.collect { |r| r.name }
me = self.class.includes(association_names).find self.id
pairs = association_names.collect do |association_name|
object_or_array = me.send(association_name)
if object_or_array.is_a? ActiveRecord::Associations::CollectionProxy
association_pairs = object_or_array.collect { |o| [o.id, o.attributes] }
[association_name, Hash[*association_pairs.flatten(1)]]
else
[association_name, object_or_array.attributes]
end
end
Hash[*pairs.flatten(1)]
end