How can I include scoped model in Rails? - ruby-on-rails

If I just including nested model in query such this
#projects = current_user.projects.all(include: :reviews)
everything ok. But Review model has some scope, that I need implement in query above. I trying this
#projects = current_user.projects.all(include: :reviews.unreaded)
and gets error. What is the right way to do this?

One option would be to create an association based on the scope, roughly:
#projects = current_user.projects.all(include: :unread_reviews)
Then create an unread_reviews association, roughly:
class Project < ...
has_many :unread_reviews, :conditions => ['read=?', true], :class_name => "Review"
(Replace the above has_many with your association particulars, obviously.)
This technique is discussed in the association docs.

Related

Any better way to execute something like this?

I'm trying to list all the user's products with a probable association where a flag 'notification' is set to zero.
user.probable_associations.where(:notified => 0).collect{|a| Product.where(:id => a.product_id).collect{|p| p.name}}.to_sentence
It seems like using a where and collect method twice within the statement isn't very good. Is there a better way to go about this?
Also, the result is something like
"[\"Product A\"] and [\"Product B\"]"
which is pretty ugly...and I still need to remove the extra punctuation "[\" \"]
instead of something clean like
"Product A and Product B"
EDIT based on Rich's Answer, still have issues because notified is a field in associations NOT product:
has_many :probable_associations, -> { where "associations.category = 3"}, class_name: 'Association', before_add: :set_probable_category
has_many :probable_products, class_name: 'Product', through: :probable_associations, source: :product do
def not_notified
select(:name).where(notified: 0)
end
end
I'd use an ActiveRecord Association extension:
#app/models/user.rb
Class User < ActiveRecord::Base
has_many :products do
def not_notified
select(:name).where(notified: 0)
end
end
end
#-> #user.products.not_notified
That's my contribution, but you could then use #spickermann & #tompave's controbutions and use .flatten.to_sentence
Without knowing what probable_associations does would I rewrite the code to something like this:
product_ids = user.probable_associations.where(:notified => 0).map(&:product_id)
Product.where(:id => product_ids).map(&:name).to_sentence
Assuming that probable_associations is just an ActiveRecord has_many association, and that you want to end up with a list of titles for Product records, you can use this:
ids = user.probable_associations
.where(notified: 0)
.pluck(:product_id)
result = Product.where(id: ids).pluck(:name).to_sentence
It's similar to #spikermann's answer, but pluck(:column_name) is faster than using a block and only extracts the required column from the DB.
Also, the reason your code produces that string is that, by the time you call to_sentence, you have an Array of sub-arrays. Each sub-array contains a single element: a product name.
That's because the second collect is sent to an ActiveRecord::Relation containing just one record.
You could have solved that problem with flatten, but the whole operation could just be refactored.

CanCan and polymorphic associations (Eager Loading error)

I'm trying to define a user's ability to access something based on a column on an associated model (so something like can :read, Step, 'steppable' => {published: true}), the problem is that it's a polymorphic association so it can't find the steppable table because it doesn't exist.
I've got steps, and each step has a steppable (either a lecture, a quiz, or some other action). I need an activerecord query that will work. I've tried:
Step.includes(:steppable).where('steppable' => {published: true})
and
Step.joins(:steppable).where('steppable' => {published: true})
But both result in ActiveRecord::EagerLoadPolymorphicError: Can not eagerly load the polymorphic association :steppable
Models look like this:
class Step < ActiveRecord::Base
...
belongs_to :steppable, polymorphic: true, dependent: :destroy
...
end
and
class Lecture
...
has_one :step, as: :steppable, dependent: :destroy
...
end
Note: I'd like to be agnostic regarding the associated model, and in order for it to work for fetching records with CanCan, it has to be done using database columns (see github.com/ryanb/cancan/wiki/defining-abilities)
You should be able to do this:
can :read, Step, steppable_type: 'Lecture', steppable_id: Lecture.published.pluck(:id)
can :read, Step, steppable_type: 'OtherThing', steppable_id: OtherThing.published.pluck(:id)
You have to do it for each Steppable class, but it gets around the eager loading polymorphic associations problem. To dry this up a bit:
[Lecture, OtherThing].each do |klass|
can :read, Step, steppable_type: klass.to_s, steppable_id: klass.published.pluck(:id)
end
In this case, as long as each steppable class has a scope published, you just add any steppable class into that array, even if published is defined differently in each class.
You can do it this way:
lectures = Lecture.where(published: true)
steps = Step.where(steppable_type: 'Lecture', steppable_id: lectures)
Or in the case you really want to be agnostic regarding the associated model:
Step.all.select { |s| s.steppable.published? }

Rails STI. How do I count the number of votes of type Upvote or Downvote for associated model?

Models are Vote and Issue. Issue has_many :votes. Vote has a STI column type that is filled with either :Upvote or :Downvote by a hidden form on the Issues index when a user votes on an issue. How do I find the number of votes of either type Upvote or Downvote for a given issue? Right now I'm using a method on the Issue model like this:
def upvotes_count(issue_id)
Upvote.count(:conditions => "issue_id = #{issue_id}")
end
def downvotes_count(issue_id)
Downvote.count(:conditions => "issue_id = #{issue_id}")
end
I pass the issue_id from the view when I cycle through all the issues on the Issues index like this:
issue.upvotes_count(issue.id)
Is this right or is there a better way? Doesn't seem like I should have to pass the id when I'm already operating on an instance. I've played around in the console trying to figure it out but can't figure it out. I know #issue.Upvote.count doesn't work.
You have a couple of options: define additional associations (or methods) in the Issue class, or use scope in the Vote class. These examples assume Rails 3, though they can be easily adjusted for Rails 2.3.
Additional associations in Issue:
class Issue < ActiveRecord::Base
has_many :votes
has_many :upvotes, :class_name => "Vote", :conditions => ['type = ?', 'Upvote']
has_many :downvotes, :class_name => "Vote", :conditions => ['type = ?', 'Downvote']
end
Here, we add conditions to refine the results of associated votes on an issue so you can call issue.upvotes and issue.downvotes. To get the count of upvotes on an issue is, in this case, issue.upvotes.count.
Scope in Vote:
class Vote
scope :up, where(:type => 'Upvote')
scope :down, where(:type => 'Downvote')
end
The up and down scopes in Vote provide methods on vote relations so you can call issue.votes.up and issue.votes.down. To get the count of upvotes on an issue is simply issue.votes.up.count.

Is there any way to use AREL for custom associations?

model Post
# ActiveRecord associations have tons of options that let
# you do just about anything like:
has_many :comments
has_many :spam_comments, :conditions => ['spammy = ?', true]
# In Rails 3, named scopes are ultra-elegant, and let you do things like:
scope :with_comments, joins(:comments)
end
Is there any way to use AREL, or an otherwise leaner syntax, to define custom associations as elegantly as named scopes?
update
I've decided it's not a good idea to put that sort of detail into an association anyway, because associations should always/mostly define the basic relationships between models.
One of the solutions is to put spammy scope on Comments:
model Post
has_many :comments
scope :with_comments, joins(:comments)
end
model Comment
scope :spammy, where(:spammy => true)
end
This looks a bit cleaner with respect to model responsibilities. Performance-wise it's exactly the same:
p.comments.spammy.to_sql
# → SELECT "comments".* FROM "comments"
# WHERE ("comments".post_id = 2) AND ("comments"."spammy" = "t")
The added benefit: you can get spammy comments from any other associations.
Well, there may be a better way, but I know that you can use actual Arel conditions (as opposed to ActiveRecord::Relations) in associations by using Arel's to_sql feature.
has_many :spam_comments, :class_name => 'Comment', :conditions => Comment.arel_table[:spammy].eq(true).to_sql
You'll notice that actual Arel code isn't as lean as ActiveRecord Relations.
I did find a comment in the Rails master branch that referred to passing an Arel predicate as a condition but that code doesn't seem to be in the 3.0 branch. At least not that I could find.

How do you scope ActiveRecord associations in Rails 3?

I have a Rails 3 project. With Rails 3 came Arel and the ability to reuse one scope to build another. I am wondering if there is a way to use scopes when defining a relationship (e.g. a "has_many").
I have records which have permission columns. I would like to build a default_scope that takes my permission columns into consideration so that records (even those accessed through a relationship) are filtered.
Presently, in Rails 3, default_scope (including patches I've found) don't provide a workable means of passing a proc (which I need for late variable binding). Is it possible to define a has_many into which a named scope can be passed?
The idea of reusing a named scope would look like:
Orders.scope :my_orders, lambda{where(:user_id => User.current_user.id)}
has_many :orders, :scope => Orders.my_orders
Or implicitly coding that named scope in the relationship would look like:
has_many :orders, :scope => lambda{where(:user_id => User.current_user.id)}
I'm simply trying to apply default_scope with late binding. I would prefer to use an Arel approach (if there is one), but would use any workable option.
Since I am referring to the current user, I cannot rely on conditions that aren't evaluated at the last possible moment, such as:
has_many :orders, :conditions => ["user_id = ?", User.current_user.id]
I suggest you take a look at "Named scopes are dead"
The author explains there how powerful Arel is :)
I hope it'll help.
EDIT #1 March 2014
As some comments state, the difference is now a matter of personal taste.
However, I still personally recommend to avoid exposing Arel's scope to an upper layer (being a controller or anything else that access the models directly), and doing so would require:
Create a scope, and expose it thru a method in your model. That method would be the one you expose to the controller;
If you never expose your models to your controllers (so you have some kind of service layer on top of them), then you're fine. The anti-corruption layer is your service and it can access your model's scope without worrying too much about how scopes are implemented.
How about association extensions?
class Item < ActiveRecord::Base
has_many :orders do
def for_user(user_id)
where(user_id: user_id)
end
end
end
Item.first.orders.for_user(current_user)
UPDATE: I'd like to point out the advantage to association extensions as opposed to class methods or scopes is that you have access to the internals of the association proxy:
proxy_association.owner returns the object that the association is a part of.
proxy_association.reflection returns the reflection object that describes the association.
proxy_association.target returns the associated object for belongs_to or has_one, or the collection of associated objects for has_many or has_and_belongs_to_many.
More details here: http://guides.rubyonrails.org/association_basics.html#association-extensions
Instead of scopes I've just been defining class-methods, which has been working great
def self.age0 do
where("blah")
end
I use something like:
class Invoice < ActiveRecord::Base
scope :aged_0, lambda{ where("created_at IS NULL OR created_at < ?", Date.today + 30.days).joins(:owner) }
end
You can use merge method in order to merge scopes from different models.
For more details search for merge in this railscast
If you're just trying to get the user's orders, why don't you just use the relationship?
Presuming that the current user is accessible from the current_user method in your controller:
#my_orders = current_user.orders
This ensures only a user's specific orders will be shown. You can also do arbitrarily nested joins to get deeper resources by using joins
current_user.orders.joins(:level1 => { :level2 => :level3 }).where('level3s.id' => X)

Resources