Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to union two different Mongoid Criteria

I have the following scopes defined in my model:

scope :upcoming, -> { where(:start_time.gt => Time.now).asc(:start_time) }
scope :in_progress, -> {
   now = Time.now
   where(:start_time.lte => now).where(:end_time.gte => now).asc(:start_time)
}

I want to create another scope that combines the results of both of those scopes called current. I tried something like this:

scope :current, -> { self.in_progress | self.upcoming }

But this just ends up treating them both like arrays and concatenating them. The problem with this is that when I try to call my scope with Model.current, I get the following error message:

NoMethodError: undefined method `as_conditions' for #<Array:0xaceb008>

This is because it converted the Mongoid Criteria object to an array, but I don't want that. I want the object to stay as a Mongoid Criteria object.

What I really want is the union of the in_progress set and the upcoming set.

Any ideas? Thanks.

like image 243
d0nutz1 Avatar asked Apr 21 '12 03:04

d0nutz1


2 Answers

You can try to compose your criteria using Mongoid's query methods and dereferencing into the criteria's selector, but I wouldn't necessarily recommend this -- see below for an example. I second the recommendation to craft your third scope. Remember that these scopes correspond to db queries that you want to be efficient, so it is probably worth your time to examine and understand the resulting and underlying MongoDB queries that are generated.

Model

class Episode
  include Mongoid::Document
  field :name, type: String
  field :start_time, type: Time
  field :end_time, type: Time

  scope :upcoming, -> { where(:start_time.gt => Time.now).asc(:start_time) }
  scope :in_progress, -> {
     now = Time.now
     where(:start_time.lte => now).where(:end_time.gte => now).asc(:start_time)
  }
  scope :current, -> { any_of([upcoming.selector, in_progress.selector]) }
  scope :current_simpler, -> { where(:end_time.gte => Time.now) }
end

Test

require 'test_helper'

class EpisodeTest < ActiveSupport::TestCase
  def setup
    Episode.delete_all
  end

  test "scope composition" do
    #p Episode.in_progress
    #p Episode.upcoming
    #p Episode.current
    #p Episode.current_simpler

    in_progress_name = 'In Progress'
    upcoming_name = 'Upcoming'
    Episode.create(:name => in_progress_name, :start_time => Time.now, :end_time => 1.hour.from_now)
    Episode.create(:name => upcoming_name, :start_time => 1.hour.from_now, :end_time => 2.hours.from_now)

    assert_equal([in_progress_name], Episode.in_progress.to_a.map(&:name))
    assert_equal([upcoming_name], Episode.upcoming.to_a.map(&:name))
    assert_equal([in_progress_name, upcoming_name], Episode.current.to_a.map(&:name))
    assert_equal([in_progress_name, upcoming_name], Episode.current_simpler.to_a.map(&:name))
  end
end
like image 79
Gary Murakami Avatar answered Oct 15 '22 11:10

Gary Murakami


You have to map your Array back to a Mongoid::Criteria. Any array of yours can be translated to a criteria with any_in:

scope :has_data, -> { any_in(:_id => all.select{ |record| record.data.size > 0 }.map{ |r| r.id }) }

So, something like this should do the trick: (untested)

scope :current, -> { any_in(:_id => (self.in_progress + self.upcoming).map{ |r| r.id }) }

I hope there exists better solutions, but this solves the equation at least.

like image 40
patrick Avatar answered Oct 15 '22 09:10

patrick