Should scopes be seperated out to allow chaining or be specific to the what you are trying to do?
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