I'm working on a Newsfeed action. I have active friendships and i would like to find the posts only of those friendships to create the newsfeed.
This is the code i currently have:
#active_friendships = current_user.active_friendships
#posts = Post.where({ :user_id => [5,8,16] }).order("created_at DESC")
I don't know how to send the #active_friendships.user_id values to the IN in the second line. At the moment the code is only worked because it has hardcoded the user_id of my active friends (5,8,16).
(If i do a debug #active_friendships) i get the 3 objects with their ids, but i still don't know how to send them to the IN in the second line as the ids to look for.
--
- !ruby/object:Friendship
attributes:
created_at: 2010-10-06 22:27:54.620007
updated_at: 2010-10-07 00:19:10.329799
id: 182
user_id: 8
status: 1
friend_id: 5
attributes_cache: {}
changed_attributes: {}
destroyed: false
marked_for_destruction: false
new_record: false
previously_changed: {}
readonly: false
- !ruby/object:Friendship
attributes:
created_at: 2010-10-07 19:13:10.617959
updated_at: 2010-10-07 19:13:17.097514
id: 192
user_id: 16
status: 1
friend_id: 5
attributes_cache: {}
changed_attributes: {}
destroyed: false
marked_for_destruction: false
new_record: false
previously_changed: {}
readonly: false
- !ruby/object:Friendship
attributes:
created_at: 2010-10-10 04:12:48.931120
updated_at: 2010-10-10 04:12:56.960752
id: 214
user_id: 8
status: 1
friend_id: 5
attributes_cache: {}
changed_attributes: {}
destroyed: false
marked_for_destruction: false
new_record: false
previously_changed: {}
readonly: false
Many thanks in advance
You can try this:
#posts = Post.where(:user_id => #active_friendships.map(&:friend_id)
).order("created_at DESC"
But better way of implementing this is to add an association
class User
has_many :active_friendships, :class_name => "Friendship", :conditions = {...}
has_many :active_friend_posts, :through => :active_friendships,
:source => :friend_posts
end
class Friendship
belongs_to :user
belongs_to :friend, :class_name => "User"
has_many :friend_posts, :class_name => "Post",
:primary_key => :freind_id, :foreign_key => :post_id
end
class Post
belongs_to :user
end
Now you can do the following:
current_user.active_friend_posts.order("created_at DESC")
I'm new at this, but could you collect all of the Friendship.user_id's into an array and then pass that in the where clause?
#friends_userids = current_user.active_friendships.collect { |x| x.user_id }
#posts = Post.where({ :user_id => #friends_userids }).order("created_at DESC")
I had a similar issue. This is how I implemented it:
ids = #active_friendships.map { |x| x.user_id }
ids = ids.join(",")
#posts = Post.all(:conditions => ["posts.user_id in (#{ids})"]).order("created_at DESC")
This creates an array of all user IDs from #active_friendships and then joins them into a string with a comma as a separator. Then I simply passed this to the :conditions parameter.
It's not as elegant as I like but it works.
Hope this helps.
Rails has a special method for getting an array of associated ids. You should be able to do this in one line something like this:
#posts = Post.where(:user_id => current_user.active_friendship_ids).order("created_at DESC")
Whenever you're dealing with has_many and has_many :through relationships, you have access to the association_ids method, which won't return the full objects. Check out the full api here:
http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html
Related
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!
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.
> u = User.first
> u.viewable_cars
OR
> Car.for(u)
would get me just the cars the user has permission to view but not the cars he owns! SQL in irb for both u.viewable_cars & Car.for(u), which is the same, cars with id 1 to 50 which belongs to user don't get called at all:
SELECT "cars".* FROM "cars" INNER JOIN "permissions" ON "permissions"."thing_id" = "cars"."id" AND "permissions"."thing_type" = $1 WHERE ((cars.user_id = 1) OR (permissions.action = 'view' AND permissions.user_id = 1)) ORDER BY created_at DESC [["thing_type", "Car"]]
=> #<ActiveRecord::Relation [#<Car id: 52, content: "sport edition", name: "BMW", user_id: 2, created_at: "2014-11-01 04:58:19", updated_at: "2014-11-01 04:58:19">, #<Car id: 51, content: "super sport car", name: "BMW M6", user_id: 3, created_at: "2014-11-01 04:44:31", updated_at: "2014-11-01 04:44:31">]>
class Car < ActiveRecord::Base
belongs_to :user
has_many :permissions, as: :thing
default_scope -> { order('created_at DESC') }
validates :user_id, presence: true
validates :name, presence: true, length: { maximum: 50 }, uniqueness: true
validates :content, length: { maximum: 300 }, allow_blank: true
scope :viewable_by, ->(user) do
joins(:permissions).where(permissions: { action: "view",
user_id: user.id })
end
scope :for, ->(user) do
joins(:permissions).
where("(cars.user_id = :user_id) OR (permissions.action = 'view' AND permissions.user_id = :user_id)", user_id: user.id)
end
end
class User < ActiveRecord::Base
...
has_many :cars, dependent: :destroy
has_many :permissions
has_many :viewable_cars, ->(user) { joins(:permissions).
where("(cars.user_id = :user_id) OR (permissions.action = 'view' AND permissions.user_id = :user_id)", user_id: user.id) },
class_name: "Car"
def viewable_cars
Car.for(self)
end
end
class Permission < ActiveRecord::Base
belongs_to :user
belongs_to :thing, polymorphic: true
end
Rails.application.routes.draw do
resources :users do
resources :cars
end
end
Your scope for in car.rb should be this:
scope :for, ->(user) do
joins("LEFT OUTER JOIN permissions ON permissions.thing_id = cars.id AND permissions.thing_type = 'Car'").where("(cars.user_id = :user_id) OR (permissions.action = 'view' AND permissions.user_id = :user_id)", user_id: user.id)
end
Now you can do: Car.for(current_user).find(params[:id]). However, this looks like an antipattern to me. So you can create another association in user.rb like this:
Rails 4:
has_many :viewable_cars, ->(user) {
joins("LEFT OUTER JOIN permissions ON permissions.thing_id = cars.id AND permissions.thing_type = 'Car'").
where("(cars.user_id = :user_id) OR (permissions.action = 'view' AND permissions.user_id = :user_id)", user_id: user.id) },
class_name: 'Car'
Rails 3(couldn't find a better way of doing it, e.g.: associations):
def viewable_cars
Car.for(self)
end
So that you can fetch all cars for user:
current_user.viewable_cars
In controller:
#car = current_user.viewable_cars.find(params[:id])
I would like to perform:
XXX.find_or_build_by_language_id(attributes)
I found
XXX.find_or_initialize_by_language_id(attributes)
but that only set language_id and no other attributes. Even if I manually sets the attributes, the record is not saved when I perform XXX.save.
I just read Rails - find or create - is there a find or build?, which seems related to my problem but does not fit my needs.
Edit
Let's use this scenario
# db/migrations/create_models.rb
class CreateModels < ActiveRecord::Migration
def self.up
create_table :companies do |t|
t.string :name
end
create_table :employees do |t|
t.string :name
t.string :city
t.references :company
end
end
end
-
# app/models/employee.rb
class Employee < ActiveRecord::Base
belongs_to :company
end
-
# app/models/company.rb
class Company < ActiveRecord::Base
has_many :employees
end
-
# rails console
:001> c = Company.new
=> #<Company id: nil, name: nil>
:002> c.employees
=> []
:003> e = c.employees.find_or_initialize_by_name(:name => 'foo', :city => 'bar')
=> #<Employee id: nil, name: "foo", city: "bar", company_id: nil>
:004> c.employees
=> []
:005> c.save
=> true
:006> c.employees
=> []
:007> e.save
=> true
:008> c = Company.first
=> #<Company id: 1, name: nil>
:009> c.employees
=> [#<Employee id: 1, name: "foo", city: "bar", company_id: 1>]
:010> e = c.employees.find_or_initialize_by_name(:name => 'foo', :city => 'baz')
=> #<Employee id: 1, name: "foo", city: "bar", company_id: 1>
:011> e.city = 'baz'
=> "baz"
:012> c.employees
=> [#<Employee id: 1, name: "foo", city: "bar", company_id: 1>]
:013 > c.save
=> true
:014> c.employees
=> [#<Employee id: 1, name: "foo", city: "bar", company_id: 1>]
Problems
:004 => The Employee from :003 is not added to c.employees
:006 => The Employee from :003 is saved with c
:010 => The city attribute of employee is not set
:014 => THe city attribute of employee is not updated when saving company
How about this?
employee_attrs = {:name => 'foo', :city => 'bar'}
e = c.employees.where(employee_attrs).first || c.employees.build(employee_attrs)
For the record, here is the implementation I came with. It can probably be simpler, but it suits my needs:
module ActiveRecord
module Associations
class AssociationCollection < AssociationProxy
alias_method :old_method_missing, :method_missing
def method_missing(method_id, *arguments, &block)
if /^find_or_build_by_([_a-zA-Z]\w*)$/ =~ method_id.to_s
names = $1.split('_and_')
find_or_build_by(names, *arguments)
else
old_method_missing(method_id, *arguments, &block)
end
end
def find_or_build_by(names, *arguments)
values = arguments[0]
throw InvalidArgument unless values.keys.first.kind_of?(String)
record = Array.new(self).find do |r|
names.inject(true) do |memo, name|
memo && (values[name].to_s == r.send(name).to_s)
end
end
if record
sanitized_values = record.send(:sanitize_for_mass_assignment, values)
sanitized_values.each {|k, v| record.send("#{k}=", v)}
else
record = build(values)
end
return record
end
end
end
end
I tried the following code for my Rails 4.2.x app.
#config/initializers/collection_proxy.rb
ActiveRecord::Associations::CollectionProxy.class_eval do
alias_method :old_method_missing, :method_missing
def method_missing(method_id, *arguments, &block)
if /^find_or_build_by([_a-zA-Z]\w*)$/ =~ method_id.to_s
names = $1.split('_and_')
find_or_build_by(names, *arguments)
else
old_method_missing(method_id, *arguments, &block)
end
end
def find_or_build_by(names, *arguments)
where(names).first || build(names)
end
end
You can use it like this.
XXX.find_or_build_by(attributes)
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