Skip to main content

Should scopes be seperated out to allow chaining or be specific to the what you are trying to do?

Rails • Asked by Stephen Sizer

I am trying to write an internal tender application and am having trouble figuring out the best approach for writing a scope.

I have the following models

class Tender < ApplicationRecord
  belongs_to :user
  belongs_to :company_detail
  has_many :questions
  has_many :assets
  has_many :tender_groups, dependent: :destroy
  has_many :groups, through: :tender_groups
  has_many :tender_contractors, dependent: :destroy
  has_many :contractor_details, through: :tender_contractors
  has_many :bids
  has_many :contractor_details_from_groups, through: :groups, source: :contractor_details

  accepts_nested_attributes_for :tender_groups
  accepts_nested_attributes_for :tender_contractors
  accepts_nested_attributes_for :assets

  validates :title, :length_days, :summary, presence: true

  scope :live, ->{ where( "tender_end_date > ?", Time.current ) }
  scope :all_public, ->{ where( public: true ) }

end

class ContractorDetail < ApplicationRecord

  filterrific :default_filter_params => { :sorted_by => 'business_name_asc' },
    :available_filters => %w[
      sorted_by
      search_query
      with_business_type_id
    ]


  self.per_page = 10

  has_many :users
  has_many :insurances
  has_many :ratings
  has_many :issues
  belongs_to :business_type
  has_and_belongs_to_many :projects
  has_one :address, as: :addressable
  has_one :pre_qualify_info
  has_many :references
  has_many :bids
  has_many :group_contractors
  has_many :groups, through: :group_contractors
  has_many :tender_contractors
  has_many :tenders, through: :tender_contractors
  has_many :tenders_by_group, through: :groups, source: :tenders

  scope :search_query, lambda { |query|
    return nil  if query.blank?
    # condition query, parse into individual keywords
    terms = query.downcase.split(/\s+/)
    # replace "*" with "%" for wildcard searches,
    # append '%', remove duplicate '%'s
    terms = terms.map { |e|
      (e.gsub('*', '%') + '%').gsub(/%+/, '%')
    }
    # configure number of OR conditions for provision
    # of interpolation arguments. Adjust this if you
    # change the number of OR conditions.
    num_or_conditions = 2
    where(
      terms.map {
        or_clauses = [
          "LOWER(contractor_details.business_name) LIKE ?",
          "LOWER(contractor_details.contact_email) LIKE ?"
        ].join(' OR ')
        "(#{ or_clauses })"
      }.join(' AND '),
      *terms.map { |e| [e] * num_or_conditions }.flatten
    )
  }

  scope :sorted_by, lambda { |sort_option|
    # extract the sort direction from the param value.
    direction = (sort_option =~ /desc$/) ? 'desc' : 'asc'
    case sort_option.to_s
    when /^business_name_/
      # Simple sort on the name colums
      order("LOWER(contractor_details.business_name) #{ direction }")
    else
      raise(ArgumentError, "Invalid sort option: #{ sort_option.inspect }")
    end
  }

  scope :with_business_type_id, lambda { |business_type_ids|
    where(:business_type_id => [*business_type_ids])
  }

  def all_tenders
    all = self.tenders
    all << self.tenders_by_group
    all.uniq
  end

  # This method provides select options for the `sorted_by` filter select input.
  # It is called in the controller as part of `initialize_filterrific`.
  def self.options_for_sorted_by
    [
      ['Name (a-z)', 'business_name_asc'],
      ['Name (z-a)', 'business_name_desc'],
      ['Business Type (a-z)', 'business_type_name_asc']
    ]
  end

  def contact_fullname
    "#{contact_first_name} #{contact_last_name}"
  end

  end

Some information on how tenders work:

Tenders can be public viewable by all contractors
Tenders have a end date (tender_end_date) where they are no longer visible to contractors

Contractors can be added to tenders via the tender_contractors (many to many)
Contractors can also be added to groups to then allow mass assignment to a tender by groups (tender_groups)

What I am looking at doing is writing a scope to pull back all tenders assigned to a given contractor that are live (not past the tender_end_date). The should include public tenders.

Should I seperate these out into individual scopes live I have started to? Public and Live, so that these can be used on their own or chained to give me exactly what I need.

Also how would I go about writing the scope to find the tenders assigned to the contractor, would this need to be written in sql?

If more detail is needed please let me know.

Thanks in advance for taking your time to look at this. I am still learning my craft.


Should I seperate these out into individual scopes live I have started to?

I think it really just depends on how your application is going to use them. If there's ever a time that you'll need them independently of each other, then I would go ahead and make them their own scopes and just chain them. I generally try to keep my scopes / methods as simple as I can for the task.

Also how would I go about writing the scope to find the tenders assigned to the contractor

If I'm reading your associations correct, since you have has_many :tenders, through: :tender_contractors wouldn't it just be ContractorDetail.find(contractor).tenders ? Outside of that, each of your groups would have their own similar queries to show all tenders that are associated with that particular group.


Hi Jacob,

Thanks for your comment. You can indeed get tenders that have been assigned to the Contractor through the method above but this is just for those assigned to them directly and not indirectly through groups they belong to.

You can do this seperately via the ContractorDetail.find(contractor).tenders_by_group, I suppose I was wondering what would be the best way to combine these?

I have a method (not yet a scope) that I was playing with on the Tender model just to see if it worked.

def all_tenders
all = self.tenders
all << self.tenders_by_group
all.uniq
end

Is this the most efficient way?


Ohhhh ok, now I see what you're after.

That's a really good question, hopefully one of the more senior devs around here will chime in. I've done very similar methods like your all_tenders method which have worked fine for the uses I've had so I just left them as-is, but as you alluded to, it could probably be made more efficient with a single SQL query.

If you happen to find a good answer elsewhere, please post the results here too. I'd like to see what's the correct way to merge these two.


I forgot these existed... have you tried using merge / or to retain the ActiveRecord::Relation instead of returning an array?

def all_tenders
  self.tenders.merge(self.tenders_by_group).uniq
end

I think or would be like this in your case if you're using AR 5+:

def all_tenders
  self.tenders.or(self.tenders_by_group)
end

See https://stackoverflow.com/a/9540911


Login or Create An Account to join the conversation.

Subscribe to the newsletter

Join 27,623+ developers who get early access to new screencasts, articles, guides, updates, and more.

    By clicking this button, you agree to the GoRails Terms of Service and Privacy Policy.

    More of a social being? We're also on Twitter and YouTube.