How to access a property from belongs_to model from a scope - ruby-on-rails

I have 2 models in my app: Person and Review each person has many reviews and each review belongs to a person. I have an attribute called grade inside of my review model and I'd like to display the average grade of each person so I wrote this scope:
scope :average_grade, -> { self.first.review.average(:grade) }
Is there a better way for doing that? Besides the 2 queries that this method needs, I also have to run another 2 queries to get the proper Person object in my controller:
def show
#average = Person.includes(:review).where(id: params[:id]).average_grade
#person = Person.includes(:review).find(params[:id])
end
How could I avoid all of those queries?

Your scope is an instance method rather than scope, since it does not return ActiveRecord::Relation object.
I suggest you to do following:
# person.rb:
def average_grade
review.average(:grade)
end
# controller:
def show
#person = Person.find(params[:id])
#average = #person.average_grade
end

# person.rb
class Person < ActiveRecord::Base
has_many :reviews
end
# review.rb
class Review < ActiveRecord::Base
belongs_to :person
scope :by_person, ->(person) { where(person_id: person) }
end
# persons_controller
class PersonsController < ApplicationController
helper_method :person
private
def person
return #person if defined? #person
#person = Person.find(params[:id])
end
end
# show.html.haml
- present(person) do |person_presenter|
%p= person_presenter.average_grade
# person_presenter.rb
class PersonPresenter < BasePresenter
present :person
def average_grade
Review.by_person(person).average(:grade)
end
end
More on presenters you could find here Railscasts PRO #287 Presenters from Scratch

Related

Filtering models that belong to other models

I've got two models: ProductCategory and Product. Product model belongs to ProductCategory, and product category can have many products accordingly. The case is that I need to implement some kind of filter that would return me Products that belong to specific ProductCategory. These are my models:
class ProductCategory < ActiveRecord::Base
has_many :products
end
class Product < ActiveRecord::Base
belongs_to :product_category
# Here I added some kind of filter method
# But I haven't managed how to use it yet
def self.filter(filter)
where(product_category_id: filter) if filter
end
end
And in my controller I added something like this:
class ProductsController < ApplicationController
def index
if params[:product]
#products = Product.filter(params[:product][:product_category]
else
#products = Product.all
end
end
end
But I can't actually manage how to use my filter and I guess I should refactor it a lot. Can anyone help me with that, please?
Assuming params[:product][:product_category] is the ProductCategory ID.
class ProductsController < ApplicationController
def index
if params[:product]
#category = ProductCategory.find(params[:product][:product_category])
#products = #category.products
else
#products = Product.all
end
end
end
If params[:product][:product_category] anything else than ID then use find_by instead of find.

Accessing additional values on has_many through Rails

I am having real trouble accessing the value of an additional parameter called permission on a has_many through. It is probably something simple.
My 3 Models are
class User < ActiveRecord::Base
has_many :players_users
has_many :players, through: :players_users
end
class Player < ActiveRecord::Base
has_many :players_users
has_many :users, through: :players_users
end
class PlayersUser < ActiveRecord::Base
belongs_to :user
belongs_to :player
validates :player_id, uniqueness: { scope: :user_id }
end
My controller saves the record without issue. Adding the permission value to the correct joining table.
def create
#players = Player.new(players_params)
#user= current_user
if #players.save
#player = Player.last
#user.save && #user.players_users.create(:player_id =>#player.id, :permission =>"owner")
redirect_to '/players'
else
render 'new'
end
end
However I seem unable to access it properly
I have tried
perm = User.find(current_user).players_users.includes(:Permission)
if perm == "owner"
Which gives an ActiveRecord::AssociationNotFoundError, association named 'Permission' was not found on PlayersUser; perhaps you misspelled it?
I have also tried
perm = User.players_users.where(player_id = #player.id && user_id = current_user)
perm.permission
or
perm = User.Player.where(player_id = #player.id && user_id = current_user)
or
perm = User.players.where(player_id = #player.id && user_id = current_user)
Which gives an undefined method error.
undefined method `Player'
I know this is something small in my setup but cannot figure out what it is. Any help appreciated.
players_users and players are associated with User object, so you can fetch the results as,
current_user.players_users.pluck(:permission)
I've solved this issue before with my own code.
I'll post that in a second, but first you need to refactor your controller, the current is inefficient:
#app/controllers/players_controller.rb
class PlayersController < ApplicationController
def create
#player = current_user.players.new players_params
if #player.save
current_user.player_users.find(#player.id).update(permission: "owner")
redirect_to players_path
end
end
private
def player_params
params.require(:player).permit(....)
end
end
To access the permission of the player_user, you'll need to use the following:
#permission = current_user.player_users.find(#player.id).permission
--
A much more DRY approach (if you're explicitly setting permission as owner each time) would be to introduce an enum into the Player model to act as a default:
#app/models/player.rb
class Player < ActiveRecord::Base
enum permission: [:owner, :member, :moderator] #-> defaults to :owner
end
This will do away with having to define the permission in the create method (unless of course you want to change it):
#app/controllers/players_controller.rb
class PlayersController < ApplicationController
def create
#player = current_user.players.new players_params
redirect_to players_path if #player.save
end
end
To understand this, you must remember that since player_users is a join association, ActiveRecord will automatically populate it if you create a player on the current_user object (current_user.players).
Association Extensions
In regard to pulling the permission data, I built a script which appends the permission to the player object (uses proxy_association.target etc):
#current
#player = current_user.players.find params[:player_id]
#permission = current_user.player_users.find(params[:player_id]).permission
#script
#player = current_user.players.find params[:player_id]
#permission = #player.permission
It works similarly to an SQL Alias column - so whilst you cannot manipulate the data, it will allow you to call #user.players.find(params[:player_id].permission.. except it's all done in memory:
#app/models/user.rb
class User < ActiveRecord::Base
has_many :player_users
has_many :players, through: :player_users, -> { extending PlayerPermission }
end
#app/models/concerns/player_permission.rb
module PlayerPermission
#Load
def load
permissions.each do |permission|
proxy_association.target << permission
end
end
#Private
private
#Permissions
def permissions
return_array = []
through_collection.each_with_index do |through,i|
associate = through.send(reflection_name)
associate.assign_attributes({permission: items[i]})
return_array.concat Array.new(1).fill( associate )
end
return_array
end
#######################
# Variables #
#######################
#Association
def reflection_name
proxy_association.source_reflection.name
end
#Foreign Key
def through_source_key
proxy_association.reflection.source_reflection.foreign_key
end
#Primary Key
def through_primary_key
proxy_association.reflection.through_reflection.active_record_primary_key
end
#Through Name
def through_name
proxy_association.reflection.through_reflection.name
end
#Through
def through_collection
proxy_association.owner.send through_name
end
#Captions
def items
through_collection.map(&:permission)
end
#Target
def target_collection
#load_target
proxy_association.target
end
end
--
As an aside, convention is to keep all aspects of a model name singular (ProductUser).

remove specific user from joined table

In Ruby on Rails I have a user models and a jobs model joined through a different model called applicants. I have a button for the users when they want to "remove their application for this job" but I don't know how to remove the specific user, and for that matter I don't know if I'm doing a good job at adding them either (I know atleast it works).
user.rb
class User < ActiveRecord::Base
...
has_many :applicants
has_many:jobs, through: :applicants
end
job.rb
class Job < ActiveRecord::Base
...
has_many :applicants
has_many:users, through: :applicants
end
applicant.rb
class Applicant < ActiveRecord::Base
belongs_to :job
belongs_to :user
end
when someone applies for a job my jobs controller is called:
class JobsController < ApplicationController
...
def addapply
#job = Job.find(params[:id])
applicant = Applicant.find_or_initialize_by(job_id: #job.id)
applicant.update(user_id: current_user.id)
redirect_to #job
end
...
end
Does that .update indicate that whatever is there will be replaced? I'm not sure if I'm doing that right.
When someone wants to remove their application I want it to go to my jobs controller again but I'm not sure what def to make, maybe something like this?
def removeapply
#job = Job.find(params[:id])
applicant = Applicant.find_or_initialize_by(job_id: #job.id)
applicant.update(user_id: current_user.id).destroy
redirect_to #job
end
does it ave to sort through the list of user_ids save them all to an array but the one I want to remove, delete the table then put them all back in? I'm unsure how this has_many works, let alone has_many :through sorry for the ignorance!
thanks!
Let's assume the user will want to remove their own application. You can do something like this:
class UsersController < ApplicationController
def show
#applicants = current_user.applicants # or #user.find(params[:id]), whatever you prefer
end
end
class ApplicantsController < ApplicationController
def destroy
current_user.applications.find(params[:id]).destroy
redirect_to :back # or whereever
end
end
And in your view:
- #applicants.each do |applicant|
= form_for applicant, method: :delete do |f|
= f.submit
Don't forget to set a route:
resources :applicants, only: :destroy
Some observations, I would probably name the association application instead of applicant. So has_many :applications, class_name: 'Applicant'.

Accessing an instance method in an active record query

Given the following two models:
class Wheel
belongs_to :car
def self.flat
where(flat: true)
end
and
class Car
has_many :wheels
def flats
self.wheels.flat
end
def has_flats?
flats.count > 0
end
I need a query for all cars with flat tires. I'm wondering why this isn't working in the cars model?:
def self.with_flats
where(:has_flats?)
end
or
def self.with_flats
where(:has_flats? == true)
end
This isn't returning the correct records. Any ideas?
Define a scope in Car model:
class Car
has_many :wheels
scope :having_flat_wheels, joins(:wheels).where("wheels.flat=?", true).uniq
......
end
Then to get all cars with flat tires:
Car.having_flat_wheels

Model and controller design approach for polymorphic assocation

Below I have outlined the structure of a polymorphic association.
In VacationsController I put some comments inline describing my current issue. However, I wanted to post this to see if my whole approach here is a little off. You can see in business_vacations_controller and staff_vacations_controller that I've had to make 'getters' for the model and controller so that I can access them from within vacations_model so I know which type of object I'm dealing with. Although it works, it's starting to feel a little questionable.
Is there a better 'best practice' for what I'm trying to accomplish?
models
vacation.rb
class Vacation < ActiveRecord::Base
belongs_to :vacationable, :polymorphic => true
end
business.rb
class Business < ActiveRecord::Base
has_many :vacations, :as => :vacationable
end
staff.rb
class Staff < ActiveRecord::Base
has_many :vacations, :as => :vacationable
end
business_vacation.rb
class BusinessVacation < Vacation
end
staff_vacation.rb
class StaffVacation < Vacation
end
controllers
business_vacations_controller.rb
class BusinessVacationsController < VacationsController
private
def controller_str
"business_schedules"
end
def my_model
BusinessVacation
end
def my_model_str
"business_vacation"
end
end
staff_vacations_controller.rb
class StaffVacationsController < VacationsController
private
def controller_str
"staff_schedules"
end
def my_model
StaffVacation
end
def my_model_str
"staff_vacation"
end
end
vacations_controller.rb
class VacationsController < ApplicationController
def create
# Build the vacation object with either an instance of BusinessVacation or StaffVacation
vacation = #class.new(params[my_model_str])
# Now here's the current issue -- I want to save the object on the association. So if it's a 'BusinessVacation' object I want to save something like:
business = Business.find(vacation.vacationable_id)
business.vacations.build
business.save
# But if it's a 'StaffVacation' object I want to save something like:
staff = Staff.find(vacation.vacationable_id)
staff.vacations.build
staff.save
# I could do an 'if' statement, but I don't really like that idea. Is there a better way?
respond_to do |format|
format.html { redirect_to :controller => controller_str, :action => "index", :id => vacation.vacationable_id }
end
end
private
def select_class
#class = Kernel.const_get(params[:class])
end
end
It feels like a lot of hoops to jump through in the VacationsController to make it aware of the context. Is there a reason that the StaffVacationsController and BusinessVacationsController couldn't each have a #create action and the views would submit to whichever is appropriate? These actions would already know the model context and be able to redirect to the appropriate url afterward.

Resources