association data in rails model scope - ruby-on-rails

I have a model named Post (blog post) and a model named Category. Each post belongs_to a category. Each category has an attribute named retainer that specifies the amount of time before a post "expires", so for example movies_category.retainer = 30.days
What I'm trying to do is create a scope for Post that finds all of the posts which are "expired". So for example, assuming I were to hardcode the value of 30.days and it were to apply to all categories (therefore all posts), the scope would be:
scope :expired, lambda { where("posts.created_at < ?", 30.days.ago) }
However, instead of hardcoding the value 30.days.ago, I want to get the retainer value from the post's category and base the condition on that, so something like:
scope :expired, lambda { where("posts.created_at < ?",
Time.now - post.category.retainer) }
So put into words: I want to get all of the posts which are expired, and the expiration status is determined by each post's category's retainer value (i.e. posts in the movies category expire in 10 days, posts in the games category expire in 5 days, etc.)
Is this even possible? Would I require some form of join or something?

scope :expired, lambda { |retainer| where("posts.created_at < ?",
Time.now - retainer) }
Then just use it like:
Post.expired(post.category.retainer)
Or from the category model like:
def Posts
Post.expired(retainer)
end

Related

Rails, Has and belongs to many, match all conditions

I have two models Article and Category
class Article < ApplicationRecord
has_and_belongs_to_many :categories
end
I want to get Articles that have category 1 AND category 2 associated.
Article.joins(:categories).where(categories: {id: [1,2]} )
The code above won't do it because if an Article with category 1 OR category 2 is associated then it will be returned and thats not the goal. Both must match.
You can query only those articles of the first category, which are also the articles of the second category.
It's going to be something like this:
Article.joins(:categories)
.where(categories: { id: 1 })
.where(id: Article.joins(:categories).where(categories: { id: 2 }))
Note, that it can be:
Category.find(1).articles.where(id: Category.find(2).articles)
but it makes additional requests and requires additional attention to the cases when category can't be found.
The way to do it is to join the same table multiple times. Here is an untested class method on Article:
def self.must_have_categories(category_ids)
scope = self
category_ids.each do |category_id|
scope = scope.joins("INNER JOIN articles_categories as ac#{category_id} ON articles.id = ac#{category_id}.article_id").
where("ac#{category_id}.category_id = ?", category_id)
end
scope
end

ActiveRecord: Get all users who should be banned today

I have two models:
class User < ActiveRecord::Base
has_many :ban_messages, dependent: :destroy
end
class BanMessage < ActiveRecord::Base
belongs_to :user
end
Table BanMessage contains a field t.datetime :ban_date that stores the date, when user should be banned. User table contains a field status, that contains one of the status values (active, suspended, banned). When I send BanMessage to User, his status field changes from 'active' to 'suspended'. Also I can activate user back, so he would not be banned at ':ban_date', but his BanMessage wouldn't be destroyed.
So I need to create query for selecting all Users with status 'suspended', who have in their newest BanMessages records 'ban_date' field, with DateTime value, which is between 'DateTime.now.beginning_of_day' and 'DateTime.now.end_of_day'.
You can filter your users with where query followed by join method for associated ban_messages with where method to further filter with the date range.
User.where(status: 'suspended').joins(:ban_messages).where("ban_date >= ? AND ban_date <= ?", DateTime.now.beginning_of_day, DateTime.now.end_of_day )
Try something like this.
User.joins(:ban_messages).where(status: :suspended).where('ban_messages.ban_date BETWEEN :begin_time AND :end_date', begin_time: DateTime.now.beginning_of_day, end_time: DateTime.now.end_of_day)
Here's what I needed:
User.joins(:ban_messages)
.where('users.status = ?', :suspended)
.where('ban_messages.created_at = (SELECT MAX(ban_messages.created_at) FROM ban_messages WHERE ban_messages.user_id = users.id)')
.where('ban_messages.ban_date BETWEEN ? and ?', DateTime.now.beginning_of_day, DateTime.now.end_of_day)
.group('users.id')
UPDATE
MrYoshiji: The caveat in your code is that if you miss one day, then
the next day you won't see the people that should have been banned the
day before. You might want to get all User where the ban_date is
lesser than DateTime.now.end_of_day, either in the same view or
another one.
So final query might be something like this
User.joins(:ban_messages)
.where('users.status = ?', :suspended)
.where('ban_messages.created_at = (SELECT MAX(ban_messages.created_at) FROM ban_messages WHERE ban_messages.user_id = users.id)')
.where('ban_messages.ban_date < ?', DateTime.now.end_of_day)
.group('users.id')

How to order records by result of grouped hash in 'belongs_to' relationship

I would like to map records and order them by the results of a hash that counts the number of records in another model that has a 'belongs_to' association with the first.
tag_follow.rb
belongs_to :tag
belongs_to :user
I have a model tag.rb with the following methods
def self.follow_counts(user)
counts = TagFollow.group(:tag_id).count
Tag.order(:tag).map{|t|
t.followed_count = counts[t.id].to_i
t
}
end
def followed_count
#followed_count ||= TagFollow.where(:tag_id => self.id).count
end
Instead of ordering the tag array by the column :tag as it currently is, I would like it to be ordered by the count that is the value in the hash of the returned counts variable, matching the key with :tag_id.
What is the easiest way to do this?
You can set up a named scope in your Tag model like this:
scope :popular, -> {
joins(:tag_follows)
.group("tags.id")
.order("COUNT(*) DESC")
}
and then use it like this:
Tag.popular.all
to get all of the Tags (that have at least 1 associated tag_follows record) in the order of how many tag_follows records they are referenced in.
If you want your collection to include Tags that have 0 associated tag_follows records, so that every Tag will be in the collection, you can use a LEFT JOIN like this:
scope :popular, -> {
joins("LEFT JOIN tag_follows ON tag_follows.tag_id = tags.id")
.group("tags.id")
.order("COUNT(tag_follows.tag_id) DESC")
}
Notice that I changed the order parameter so that Tags with 0 tag_follows records will be ranked after Tags with 1 tag_follows record.

Rails scope without using SQL

I'm trying to set up a scoped query where I can find customers that have paid (or partially paid) an invoice.
However, the value I want to use in the scope isn't actually in the database, it's a method.
For example:
Bidder.rb
class Bidder < ActiveRecord::Base
...
scope :unpaid, where(payment_status: 'unpaid')
...
def payment_status
"paid" if whatever
"partial" if whatever
"unpaid" if whatever
end
...
end
When I try to use:
#auction.bidders.unpaid
I see this:
SQLite3::SQLException: no such column: bidders.payment_status: SELECT COUNT(*) FROM "bidders" WHERE "bidders"."auction_id" = 7 AND "bidders"."payment_status" = 'unpaid'
What do I need to change to get something like this to work? Should I even be using scopes?
Also, how can I change that scope to search for both 'unpaid' and 'partial' values?
Thanks
You could use scope like this:
scope :unpaid, all.select{ |bidder| bidder.payment_status == 'unpaid' }
This way you will have an array of unpaid bidders, but if you want to chain methods you have to convert that array to a relation like this:
scope :unpaid, where(id: all.select{ |bidder| bidder.payment_status == 'unpaid' }.map(&:id))
See that you are hitting database eagerly, you maybe want to have payment_status as a computed value for performance.

How can I show closest date-to-happen posts?

I am building a site that displays a weekly resume of what will be going on.
My problem is: an admin can place a post two months from now, how can I restrict those posts from showing up until the week it would actually happen?
Use a scope or two:
class Post < ActiveRecord::Base
scope :future, lambda { where('ends_at > ?', Time.zone.now') }
scope :up_to, lambda { |time| where('starts_at < ?', time) }
end
#coming_up_posts = Post.future.up_to(7.days.from_now)

Resources