Rails, how to merge two constraints with has_many :through - ruby-on-rails

I want to constraint scope of attachments to current_user attachments or attachments that are not yet assigned to comment, to be able to find later
Attachment has_one :comment, optional: true
Attachment has_one :user, through :comment, comment - through task, task - through project
> user.attachments.to_sql
=> "SELECT \"attachments\".* FROM \"attachments\" INNER JOIN \"comments\" ON \"attachments\".\"comment_id\" = \"comments\".\"id\" INNER JOIN \"tasks\" ON \"comments\".\"task_id\" = \"tasks\".\"id\" INNER JOIN \"projects\" ON \"tasks\".\"project_id\" = \"projects\".\"id\" WHERE \"projects\".\"user_id\" = $1 ORDER BY \"comments\".\"created_at\" ASC, tasks.position"
How to make it work
> user.attachments.or(Attachment.where(comment_id: nil))
Relation passed to #or must be structurally compatible. Incompatible values: [:order, :joins, :references]
I have tried
without_comment = Attachment.joins(comment: [task: [:project]]).where(comment_id: nil).order('"comments"."created_at" ASC, tasks.position')
user.attachments.or(without_comment)
[35] pry(#<AttachmentResource>)> without_comment.to_sql
=> "SELECT \"attachments\".* FROM \"attachments\" INNER JOIN \"comments\" ON \"comments\".\"id\" = \"attachments\".\"comment_id\" INNER JOIN \"tasks\" ON \"tasks\".\"id\" = \"comments\".\"task_id\" INNER JOIN \"projects\" ON \"projects\".\"id\" = \"tasks\".\"project_id\" WHERE \"attachments\".\"comment_id\" IS NULL ORDER BY \"comments\".\"created_at\" ASC, tasks.position"
[36] pry(#<AttachmentResource>)> user.attachments.or(without_comment)
ArgumentError: Relation passed to #or must be structurally compatible. Incompatible values: [:order, :joins, :references]
from /home/bjorn/.rvm/gems/ruby-2.4.1#ngrx-todolist/gems/activerecord-5.1.1/lib/active_record/relation/query_methods.rb:655:in `or!'
I want to find all current_user attachments plus attachments without owner -> no parent comment
Query Attachment.joins(comment: [task: [:project]]).where('projects.user_id = ? OR attachments.comment_id IS NULL', user.id) doesnt include attachments without comment, why?
[10] pry(#<CommentResource>)> Attachment.all
=> [#<Attachment:0x00000008a6f3a8
id: 1,
file: "attachments.rb",
comment_id: 1,
created_at: Sun, 21 May 2017 14:18:21 UTC +00:00,
updated_at: Sun, 21 May 2017 14:18:21 UTC +00:00>,
#<Attachment:0x00000008a6f268
id: 2,
file: "attachments.rb",
comment_id: 1,
created_at: Sun, 21 May 2017 14:18:21 UTC +00:00,
updated_at: Sun, 21 May 2017 14:18:21 UTC +00:00>,
#<Attachment:0x00000008a6f128
id: 3,
file: "attachments.rb",
comment_id: nil,
created_at: Sun, 21 May 2017 14:29:51 UTC +00:00,
updated_at: Sun, 21 May 2017 14:29:51 UTC +00:00>]
[11] pry(#<CommentResource>)> Attachment.joins(comment: [task: [:project]]).where('projects.user_id = ? OR attachments.comment_id IS NULL', user.id).to_sql
=> "SELECT \"attachments\".* FROM \"attachments\" INNER JOIN \"comments\" ON \"comments\".\"id\" = \"attachments\".\"comment_id\" INNER JOIN \"tasks\" ON \"tasks\".\"id\" = \"comments\".\"task_id\" INNER JOIN \"projects\" ON \"projects\".\"id\" = \"tasks\".\"project_id\" WHERE (projects.user_id = 1 OR attachments.comment_id IS NULL)"
[12] pry(#<CommentResource>)> Attachment.joins(comment: [task: [:project]]).where('projects.user_id = ? OR attachments.comment_id IS NULL', user.id)
=> [#<Attachment:0x0000000891c7a8
id: 1,
file: "attachments.rb",
comment_id: 1,
created_at: Sun, 21 May 2017 14:18:21 UTC +00:00,
updated_at: Sun, 21 May 2017 14:18:21 UTC +00:00>,
#<Attachment:0x0000000891c668
id: 2,
file: "attachments.rb",
comment_id: 1,
created_at: Sun, 21 May 2017 14:18:21 UTC +00:00,
updated_at: Sun, 21 May 2017 14:18:21 UTC +00:00>]

Thanks to #fanta
class AttachmentPolicy < ApplicationPolicy
class Scope < Scope
def resolve
# user can see his attachments and attachments without owner
Attachment
.left_outer_joins(comment: [task: [:project]])
.where('projects.user_id = ? OR comment_id IS NULL', user.id)
end
end
def update?
# if attachment doesnt have owner - anyone can change it
record.comment.nil? ? true : owner?
end
end

Related

How to limit an association collection in rails console with "as_json" Active Record methods?

I'd like to display in rails console a limit collections (4 level) of the parent entity "Article", which would be a relationship between itself, and the example:
# article.rb
class Article < ActiveRecord::Base
belongs_to :parent, :class_name => 'Article', optional: true
has_many :sub_articles, :class_name => 'Article', :foreign_key => 'parent_id'
end
def as_json(options={})
super(methods: [:subarticles])
end
where the as_json (ActiveRecord) method is overwritten to show the recursion in the relation, for example:
# rails console
Article.first.as_json
=>
id: 1,
name: "king article"
created_at: Fri, 13 Aug 2021 13:14:26.463429000 UTC +00:00,
updated_at: Fri, 13 Aug 2021 13:14:26.463429000 UTC +00:00,
parent_id: nil
sub_articles:
[ Article:0x0000641428defb71
id: 2,
name: "pencils article"
created_at: Fri, 13 Aug 2021 13:14:26.463429000 UTC +00:00,
updated_at: Fri, 13 Aug 2021 13:14:26.463429000 UTC +00:00,
parent_id: 1
sub_articles:
[ Article:0x0000621438defb71
id: 3,
name: "pencil child 1"
created_at: Fri, 13 Aug 2021 13:14:26.463429000 UTC +00:00,
updated_at: Fri, 13 Aug 2021 13:14:26.463429000 UTC +00:00,
parent_id: 2
sub_articles: [],
And that can be extended even further, but I would like to extend the sub-article up to 4 levels... to such an extent that if a father has a great-great-grandson (sub_articles) it cannot be displayed
# in article.rb
def as_json(options={})
super(methods: [:limited_hierarchy])
end
def level_in_hierarchy
return 0 if parent_id.nil?
return 1 if parent.parent_id.nil?
return 2 if parent.parent.parent_id.nil?
return 3 if parent.parent.parent.parent_id.nil?
# ... etc
end
def limited_hierarchy
return sub_articles if level_in_hierarchy < 4
[]
end
should work, the level_in_hierarchy method can be refactored to make it more performant, but I've left it this way for readability. We are only interested in whether the level_in_hierarchy is < 4, so it's not necessary to calculate the value for all levels.
Depending on the number of articles in the application, it might be a better choice to set the value of level_in_hierarchy as an attribute in the Article model at the time that the model is created. This will improve performance.
I think a little adjustion of Les Nightingill's answer can do what you want.
Adding attr_accesstor :level_in_hierarchy:
# in article.rb
attr_accesstor :level_in_hierarchy
def as_json(options={})
super(methods: [:limited_hierarchy])
end
def limited_hierarchy
level_in_hierarchy ||= 0
if level_in_hierarchy < 4
sub_articles.each { |sub_a| sub_a.level_in_hierarchy = level_in_hierarchy + 1 }
sub_articles
else
[]
end
end

How to display all association collection in rails console?

I'd like to display in rails console all the collections of the parent entity "Article", which would be a relationship between itself, example:
# article.rb
class Article < ActiveRecord::Base
belongs_to :parent, :class_name => 'Article', optional: true
has_many :sub_articles, :class_name => 'Article', :foreign_key => 'parent_id'
end
Now what I have is:
irb(main):095:0> Article.find(1)
Article Load (0.9ms) SELECT "articles".* FROM "articles" ORDER BY
"articles"."id" ASC LIMIT $1 [["LIMIT", 1]]
=>
#<Articles:0x0000264231faa292
id: 1,
name: "king article"
created_at: Fri, 13 Aug 2021 13:14:26.463429000 UTC +00:00,
updated_at: Fri, 13 Aug 2021 13:14:26.463429000 UTC +00:00,
parent_id: nil
What I'd like to display on the rails console:
id: 1,
name: "king article"
created_at: Fri, 13 Aug 2021 13:14:26.463429000 UTC +00:00,
updated_at: Fri, 13 Aug 2021 13:14:26.463429000 UTC +00:00,
parent_id: nil
sub_articles:
[ Article:0x0000641428defb71
id: 2,
name: "pencils article"
created_at: Fri, 13 Aug 2021 13:14:26.463429000 UTC +00:00,
updated_at: Fri, 13 Aug 2021 13:14:26.463429000 UTC +00:00,
parent_id: 1
sub_articles:
[ Article:0x0000621438defb71
id: 3,
name: "pencil child 1"
created_at: Fri, 13 Aug 2021 13:14:26.463429000 UTC +00:00,
updated_at: Fri, 13 Aug 2021 13:14:26.463429000 UTC +00:00,
parent_id: 2
sub_articles: [],
id: 4,
name: "pencil child 2"
created_at: Fri, 13 Aug 2021 13:14:26.463429000 UTC +00:00,
updated_at: Fri, 13 Aug 2021 13:14:26.463429000 UTC +00:00,
parent_id: 2
sub_articles: []
]
]
Ultimately what I am looking for is if a parent is consulted to show their children (and the children of the children if possible)
It's probably best achieved by calling to_json on the top-level article, and configuring the as_json method of Article to include sub_articles
class Article < ActiveRecord::Base
def as_json(options={})
super(methods: [:subarticles])
end
end
in the rails console:
$> Article.find(3).to_json # => should give you the hierarchy you're looking for

Ruby How to iterate over an array of instances

I have this array from Order.all
[#<Order:0x00007f1d219f7028 id: 1, time: "01.00", amount: 21, created_at: Mon, 16 Apr 2018 17:44:41 UTC +00:00, updated_at: Mon, 16 Apr 2018 17:44:41 UTC +00:00>,
#<Order:0x00007f1d219f6ee8 id: 2, time: "02.00", amount: 23, created_at: Mon, 16 Apr 2018 17:44:41 UTC +00:00, updated_at: Mon, 16 Apr 2018 17:44:41 UTC +00:00>]
When I do Order.all.first[:time] I get "01.00" so that 'works'.
But when I do
a = []
Order.all.each do |e|
b = Array(e[:time])
b << e[:amount]
a << b
end
I just get the above array again???
How do I iterate over the array to get
[['01.00', 21], ['02.00', 23]]
You don't need that. Try this one
Order.all.pluck(:time, :amount)
A more verbose and expensive way to do it
Order.all.map { |order| [order.time, order.amount] }
Even more verbose, probably what you're trying to do
result = []
Order.all.each do |order|
result << [order.time, order.amount]
end
Your solution may be correct. You should keep in mind that #each return the enumerated collection. The collection of orders in your case.
The result you are expecting is the variable a

Rails: find attachments without user OR without comment

Attachment has_one :comment, optional: true
Attachment has_one :user, through :comment, comment - through task, task - through project
I want to find all current_user attachments plus attachments without owner -> no parent comment
Query Attachment.joins(comment: [task: [:project]]).where('projects.user_id = ? OR attachments.comment_id IS NULL', user.id) doesnt include attachments without comment, why?
[10] pry(#<CommentResource>)> Attachment.all
=> [#<Attachment:0x00000008a6f3a8
id: 1,
file: "attachments.rb",
comment_id: 1,
created_at: Sun, 21 May 2017 14:18:21 UTC +00:00,
updated_at: Sun, 21 May 2017 14:18:21 UTC +00:00>,
#<Attachment:0x00000008a6f268
id: 2,
file: "attachments.rb",
comment_id: 1,
created_at: Sun, 21 May 2017 14:18:21 UTC +00:00,
updated_at: Sun, 21 May 2017 14:18:21 UTC +00:00>,
#<Attachment:0x00000008a6f128
id: 3,
file: "attachments.rb",
comment_id: nil,
created_at: Sun, 21 May 2017 14:29:51 UTC +00:00,
updated_at: Sun, 21 May 2017 14:29:51 UTC +00:00>]
[11] pry(#<CommentResource>)> Attachment.joins(comment: [task: [:project]]).where('projects.user_id = ? OR attachments.comment_id IS NULL', user.id).to_sql
=> "SELECT \"attachments\".* FROM \"attachments\" INNER JOIN \"comments\" ON \"comments\".\"id\" = \"attachments\".\"comment_id\" INNER JOIN \"tasks\" ON \"tasks\".\"id\" = \"comments\".\"task_id\" INNER JOIN \"projects\" ON \"projects\".\"id\" = \"tasks\".\"project_id\" WHERE (projects.user_id = 1 OR attachments.comment_id IS NULL)"
[12] pry(#<CommentResource>)> Attachment.joins(comment: [task: [:project]]).where('projects.user_id = ? OR attachments.comment_id IS NULL', user.id)
=> [#<Attachment:0x0000000891c7a8
id: 1,
file: "attachments.rb",
comment_id: 1,
created_at: Sun, 21 May 2017 14:18:21 UTC +00:00,
updated_at: Sun, 21 May 2017 14:18:21 UTC +00:00>,
#<Attachment:0x0000000891c668
id: 2,
file: "attachments.rb",
comment_id: 1,
created_at: Sun, 21 May 2017 14:18:21 UTC +00:00,
updated_at: Sun, 21 May 2017 14:18:21 UTC +00:00>]
The joins method on active record queries the records by using inner join, which means in your case it only returns attachments with comments. If you want to include attachments without comments, you need to left outer join.
http://guides.rubyonrails.org/active_record_querying.html#left-outer-joins

Twitter like following in Rails

I'm following along with this tutorial to create Twitter like following in my app, however, my following isn't reciprocal, in fact, the other side doesn't follow a user at all. My two main models are User and Stock. A user should be able to 'follow' a stock, but a stock does not ever follow a user.
models/user.rb
class User < ActiveRecord::Base
has_many :stock_relationships
has_many :stocks, through: :stock_relationships, source: :user
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable,
:omniauthable, :omniauth_providers => [:facebook, :twitter, :linkedin, :google_oauth2]
# stock following/unfollowing
def follow_stock(stock)
stock_relationships.create(stock_id: stock.id)
end
def unfollow_stock(stock)
stock_relationships.find_by(stock_id: stock.id).destroy
end
def following_stock?(stock)
stock_relationships.include?(stock.id)
end
end
Since my Stock model doesn't really belong to anyone I don't have anything in that model yet:
models/stock.rb
class Stock < ActiveRecord::Base
end
To keep track of the users that are following stocks, I've created another model called StockRelationships:
models/stock_relationship.rb
class StockRelationship < ActiveRecord::Base
belongs_to :user
end
I'm starting off in console by assigning a user:
user = User.find(1)
I'm having trouble because this doesn't seem to be working when using the following_stock? method.
I can create a stock_relationship:
pry(main)> user.follow_stock(stock)
(0.2ms) BEGIN
SQL (2.2ms) INSERT INTO "stock_relationships" ("stock_id", "user_id", "created_at", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "id" [["stock_id", 2], ["user_id", 1], ["created_at", "2016-07-12 01:01:00.552580"], ["updated_at", "2016-07-12 01:01:00.552580"]]
(2.5ms) COMMIT
=> #<StockRelationship:0x007ff0b960ba60
id: 3,
user_id: 1,
stock_id: 2,
created_at: Tue, 12 Jul 2016 01:01:00 UTC +00:00,
updated_at: Tue, 12 Jul 2016 01:01:00 UTC +00:00>
and I can unfollow a stock:
[10] pry(main)> user.unfollow_stock(stock)
StockRelationship Load (0.4ms) SELECT "stock_relationships".* FROM "stock_relationships" WHERE "stock_relationships"."user_id" = $1 AND "stock_relationships"."stock_id" = $2 LIMIT 1 [["user_id", 1], ["stock_id", 2]]
(0.1ms) BEGIN
SQL (0.3ms) DELETE FROM "stock_relationships" WHERE "stock_relationships"."id" = $1 [["id", 3]]
(1.5ms) COMMIT
=> #<StockRelationship:0x007ff0b9d36a60
id: 3,
user_id: 1,
stock_id: 2,
created_at: Tue, 12 Jul 2016 01:01:00 UTC +00:00,
updated_at: Tue, 12 Jul 2016 01:01:00 UTC +00:00>
However, I'm running into problems when I'm checking to see if a user is actually following a stock:
[13] pry(main)> user.following_stock?(stock)
=> false
Which should be returning true since the user is in fact following the stock:
[15] pry(main)> StockRelationship.all
StockRelationship Load (0.4ms) SELECT "stock_relationships".* FROM "stock_relationships"
=> [#<StockRelationship:0x007ff0bfa00408
id: 2,
user_id: 1,
stock_id: 1,
created_at: Tue, 12 Jul 2016 00:32:55 UTC +00:00,
updated_at: Tue, 12 Jul 2016 00:32:55 UTC +00:00>,
#<StockRelationship:0x007ff0bfa002c8
id: 4,
user_id: 1,
stock_id: 2,
created_at: Tue, 12 Jul 2016 01:04:06 UTC +00:00,
updated_at: Tue, 12 Jul 2016 01:04:06 UTC +00:00>]
Am I implementing include? incorrectly? Have I messed up something in my models?
Thanks in advance!
Your error is a datatype comparison problem. Try this:
def following_stock?(stock)
# either use this
stock_relationships.where(stock_id: stock.id).present?
# OR
stock_relationships.pluck(:stock_id).include?(stock.id)
end
I am not at a computer in which i can test at this very moment, however looking at your code, I get a sneaky suspicion the cause may be the source declaration in your has_many through relation.
In your User model you have source: :user which i believe is making the relation look for the user_id and not the stock_id when searching from the User side of the relation.
You may want to change it to source: :stock, or in your case as the model name and relation are the same, it might be okay for you to leave out the source decleration completely.

Resources