active record find returning #<Enumerator: ...> - ruby-on-rails-5

Rails 5.x
Model School :
class School
has_many: students
def all_students
students.active + students.inactive
end
end
and student model
class Student
scope :active, -> { where(active: true) }
scope :active, -> { where(active: false) }
end
and this
my_school = School.find(1)
my_school.students.find(student_id) #returns `#<Enumerator: ...>`
PS: I can fix this changing my_school.students.find(some_id) but I can't skip the all_students method.

Related

Rails scope - how to return instances where their children match a certain value exclusively?

How can I write a rails scope that returns all instances of Company where its Employees status are ALL 'active' ?
class Employee < ApplicationRecord
STATUS = ['active', 'busy', 'inactive']
belongs_to: :company
end
class Company < ApplicationRecord
has_many: :employees
end
I tried the following scope :
scope :with_active_employees_only, -> {
select("DISTINCT ON (companies.id) companies.*")
.joins(:employees)
.where("employees.status NOT IN (?)", Employee::STATUS - ['active'])
}
But it still returns me Companies where some of the employees are 'busy' or 'inactive', even though I only want companies where its employees are exclusively 'active'. How can I achieve that ?
If you are on the latest 6.1.x version of activerecord, you can use where.missing with an aptly built association:
class Company
has_many :inactive_employees, -> { where(status: 'inactive') },
class_name: 'Employee', inverse_of: nil
# includes companies with no employee at all
scope :without_inactive_employees, -> {
where.missing(:inactive_employees)
}
end
If you are not there, you can still backport it from https://github.com/rails/rails/blob/6-1-stable/activerecord/lib/active_record/relation/query_methods.rb#L71-L80
you could group employees by company then count all status in which active is 1 otherwise 0, so that the company which has all employees active is the one has the status-count is 1.
scope :with_active_employees_only, -> {
Company.joins(:employees)
.select("companies.*")
.group("companies.id")
.having("SUM(CASE WHEN employees.status = 'active' THEN 1 ELSE 0 END) = 1")
}

Scope on average value

I'm trying to define a scope on my Movie model in order to select all movies with an average rating higher then the provided value.
So far I have the following models:
class Movie < ActiveRecord::Base
# Callbacks & Plugins
# Associations
has_and_belongs_to_many :categories
has_many :ratings
# Validations
validates :name, presence: true, uniqueness: true
validates :description, presence: true
# Scopes
scope :category, -> (category) { joins(:categories).where("categories.id = ?", category) }
scope :searchable, -> (query) { where("name LIKE '%?%'", query) }
scope :rating, -> (rating) { joins(:ratings).average("ratings.value")) }
end
class Rating < ActiveRecord::Base
# Callback & plugins
# Associations
belongs_to :user
belongs_to :movie, counter_cache: true
# Validations
validates :value, presence: true, numericality: { only_integer: true, greater_than_or_equal_to: 1, less_than_or_equal_to: 5 }
validates :user, presence: true, uniqueness: { scope: :movie_id }
end
Now I'm playing around with the query options in Rails.
What I want to do is have a scope that selects all ratings for the particular movie. Calculates the average using the value property of the rating. And if that value is equal or higher to the provided value, that movie is selected.
As in the code i've been playing with the joins and average query options, but I'm not sure how to combine them in getting what I want.
Think I found it...
scope :rating, -> (rating) { joins(:ratings).group("movies.id").having("AVG(ratings.value) > ? OR AVG(ratings.value) = ?", rating, rating) }
Generates the following query for me:
Movie Load (1.9ms) SELECT "movies".* FROM "movies" INNER JOIN "ratings" ON "ratings"."movie_id" = "movies"."id" GROUP BY movies.id HAVING AVG(ratings.value) > 1 OR AVG(ratings.value) = 1
Which is what I want I think. Will test it with some Rspec now to see if it works.

Many to many relationship with ability to set a state (active)

I have a fully working (for some time now) many-to-many relationship in my Rails application.
Instructors has many Schools (through SchoolsToInstructorsAssociations)
Schools has many Instructors (through SchoolsToInstructorsAssociations)
At this time, I would like the ability to have an "active state" in addition to simply adding or removing an Instructor from a School or a School from an Instructor.
I want an Instructor to be set as inactive before being removed completely at a later point (or reactivated).
My first thought was to add an 'active' boolean to the relationship model (SchoolsToInstructorsAssociations), but there's no simple way to access this attribute to update or query it).
My second thought was to simply create another relationship model with the 'active' attribute, but it's redundant and something extra I have to track.
Maybe a custom many-to-many module? Create a SchoolsToInstructorsAssociations controller?
class Instructor < ActiveRecord::Base
has_many :schools_to_instructors_association
has_many :schools, :through => :schools_to_instructors_association
end
class School < ActiveRecord::Base
has_many :schools_to_instructors_association
has_many :instructors, :through => :schools_to_instructors_association
end
class SchoolsToInstructorsAssociation < ActiveRecord::Base
belongs_to :user
belongs_to :school
end
I also plan to create a history record each time an instructors 'active' state changes or an instructor is removed or added to a school. Not asking how to do this, but wondering if it could be used to track an instructors 'active' state.
class SchoolsController < ApplicationController
def instructors_index
#school = School.find(params[:id])
instructors = find_instructors
#active_instructors = instructors[0]
#inactive_instructors = instructors[1]
respond_to do |format|
format.html # index.html.erb
format.json { render json: #schools }
end
end
private
def find_instructors
active = []; inactive = []
#school.instructors.each do |s|
if SchoolsToInstructorsAssociationRecord.where(user_id: s, school_id: #school)[0].active?
active << s
else
inactive << s
end
return [active, inactive]
end
end
end
class SchoolsToInstructorsAssociationRecord < ActiveRecord::Base
default_scope order('created_at DESC')
attr_accessor :user_id, :school_id, schools_to_instructors_association_id, :active
end
Sounds like you can accomplish what you're trying to do with scopes. Add a boolean column for 'active' as you described for the 'Instructor' class, then you can add scopes for it:
class Instructor < ActiveRecord::Base
...
scope :active, -> { where(active: true) }
scope :inactive, -> { where(active: false) }
...
end
Then for a given School, you can get the active (or inactive) instructors for that school:
#school.instructors.active
=> SELECT "instructors".* FROM "instructors" WHERE "instructors"."school_id" = <id> AND "instructors"."active" = 't'
If you wanted to do some operations on all the inactive instructors (like destroy them, as an example), you could do:
Instructor.inactive.map(&:destroy)
And you can of course write whatever custom methods you want for the Instructor or School classes.

How to override the default has_many condition for a relation in rails?

I would like to enter my own condition for a has_many relationship in my ActiveRecord model.
I want my condition to override the default condition.
Class User < ActiveRecord::Base
has_many :notifs, :conditions =>
proc { "(notifs.user_id = #{self.id} OR notifs.user_id = 0)" }
And it generates:
Notif Load (0.2ms) SELECT notifs.* FROM notifs WHERE
notifs.user_id = 1 AND ((notifs.user_id = 1 OR notifs.user_id =
0))
I don't want the default condition of active record (the first WHERE notifs.user_id = 1 outside parens). I want only my own. How do I specify that ?
For rails 4.1+ you can use unscope inside the association scope.
class Post
belongs_to :user
end
class User
has_many :posts, -> { unscope(:where).where(title: "hello") }
end
User.first.posts
# => SELECT "posts".* FROM "posts" WHERE "posts"."title" = $1 [["title", "hello"]]
Replace :conditions with :finder_sqllike so:
has_many :notifs,
:finder_sql => proc { "(notifs.user_id = #{self.id} OR notifs.user_id = 0)" }
More details in the documentation: http://apidock.com/rails/ActiveRecord/Associations/ClassMethods/has_many

rails 3 validations uniqueness on attributes of an association

i have a model called Fund and a model called Company .. where fund belongs_to a company.
i have this validation in my Fund table:
validates :name, presence: true, uniqueness: true
This works both on server side and client side using client_side_validations. But i want my fund names to be unique across both fund.name values and fund.company.name values. And i want to do it in a way it would work with client_side_validations too.
Suggestions?
Ended up creating a very specific validator and adding it to client-side-validation. Here'z the breakdown
In models/fund.rb
validates_fund_name_not_company_name :name
new file in config/initializers/validators .. called fund_name_not_company_name_validator.rb
class FundNameNotCompanyNameValidator < ActiveModel::EachValidator
def validate_each(record, attr_name, value)
if ::Company.exists?(name: value)
record.errors.add(attr_name, :fund_name_not_company_name, options.merge(:value => value))
end
end
end
# This allows us to assign the validator in the model
module ActiveModel::Validations::HelperMethods
def validates_fund_name_not_company_name(*attr_names)
validates_with FundNameNotCompanyNameValidator, _merge_attributes(attr_names)
end
end
module ClientSideValidations::Middleware
class FundNameNotCompanyName < ClientSideValidations::Middleware::Base
def response
if ::Company.exists?(name: request.params[:name])
self.status = 404
else
self.status = 200
end
super
end
end
end
then in app/assets/javascripts/rails.validations.custom.js
clientSideValidations.validators.remote['fund_name_not_company_name'] = function(element, options) {
if ($.ajax({
url: '/validators/fund_name_not_company_name',
data: { name: element.val() },
// async must be false
async: false
}).status == 404) { return options.message; }
}
This helped a great deal