Pundit and ActiveRecord merged joins - ruby-on-rails

I am using the Pundit gem to role scope my application and have found some difficulties merging ActiveRecord queries. I am working with Rails 5.1.4.
See I have three models, lets say Classroom, Student and Exam with:
Classroom has_many :students & has_many :exams, through: :students
Student belongs_to: :classroom & has_many :exams
Exam belongs_to: :student
Each have a policy scope with varying queries that I want to merge together to ensure a user has access to the Exam model that's within the Student scope, that's also within the Classroom scope.
Pundit let us do that by passing whatever ActiveRecord relationship from the controller to the Scope class inside a scope variable.
With that in mind, my scope classes appear as follow:
class ClassroomPolicy::Scope
def resolve
if user.reviewer?
# A reviewer can only see specific classes
scope.where(type: :exam).where(reviewer_id: user.id)
end
end
end
class StudentPolicy::Scope
def resolve
if user.reviewer?
# A reviewer can only see students from classes he's allowed in
scope.joins(:classroom).merge(policy_scope(Classroom))
end
end
end
class ExamPolicy::Scope
def resolve
if user.reviewer?
# A reviewer can only see/grade exams from students he supervised
scope.joins(:student).merge(policy_scope(Student))
end
end
end
All works well, if I do policy_scope(Exam), I do get all exams from students who where inside a reviewer's classroom.
The issue arises when passing an already joined query such as policy_scope(Classroom.find(params[:classroom_id]).exams).
ActiveRecord effectively generates a strange query which is unusable because both a_classroom.exams and our scope do a join on :student.
In addition, removing joins from our exam scope, giving us scope.merge(policy_scope(Student)), makes our call policy_scope(Classroom.find(params[:classroom_id]).exams) work while breaking policy_scope(Exam).
Is there a way to work around this so that both use cases work ?
Or is my approach to Pundit wrong ?
Is this the limit of ActiveRecord ?
Any help regarding this would be appreciated !

policy_scope is intended to be used from Controllers. For e.g. refer the examples given in section https://github.com/varvet/pundit#scopes and you should find that this method is being demonstrated to be used either from controllers or views.
However as per your code you are trying to use policy_scope in your scope classes (i.e classes extending ApplicationPolicy::Scope class) themselves. And I think I am understanding the intention behind doing so to reuse the already defined scopes. But in my opinion that's a wrong way to think about it.
Once inside YourScopeClass#resolve method you should use normal querying mechanism like you do elsewhere in your Rails application.
I don't have a complete know-how of the fields you have in your model. But based on the comments you have added in resolve method for a reviewer, you should do something like:
class StudentPolicy::Scope
def resolve
if user.reviewer?
# A reviewer can only see students from classes he's allowed in
arel = scope.joins(:classroom)
arel = arel.where(classrooms: { type: : exam, reviewer_id: user.id })
arel
end
end
end
Then use it from controller like policy_scope(Student)
Similarly
class ExamPolicy::Scope
def resolve
if user.reviewer?
# A reviewer can only see/grade exams from students he supervised
arel = arel.joins(student: [ :classroom ])
arel = arel.where(classrooms: { type: : exam, reviewer_id: user.id })
arel
end
end
end
Then use it from controller like policy_scope(Exam)
And if you want to reuse some queries then try creating some generic methods which can be passed the query represented by Pundit Policy Scope.
For e.g.
class MyClass
class << self
def accessible_students(arel:, user_id:)
arel.where(classrooms: { type: : exam, reviewer_id: user_id })
end
end
end
class StudentPolicy::Scope
def resolve
if user.reviewer?
# A reviewer can only see students from classes he's allowed in
arel = scope.joins(:classroom)
arel = MyClass.accessible_students(arel: arel, user_id: user.id)
arel
end
end
end
class ExamPolicy::Scope
def resolve
if user.reviewer?
# A reviewer can only see/grade exams from students he supervised
arel = arel.joins(student: [ :classroom ])
arel = MyClass.accessible_students(arel: arel, user_id: user.id)
arel
end
end
end
I hope you find this helpful.

Related

Compare ActiveRecord::Base and ActiveRecord

What is difference between ActiveRecord::Base and ActiveRecord?
And why we have to extend ActiveRecord::Base but not ActiveRecord ?
class User < ActiveRecord::Base
def self.authenticate_unsafely(user_name, password)
where("user_name = '#{user_name}' AND password = '#{password}'").first
end
def self.authenticate_safely(user_name, password)
where("user_name = ? AND password = ?", user_name, password).first
end
def self.authenticate_safely_simply(user_name, password)
where(user_name: user_name, password: password).first
end
end
http://api.rubyonrails.org/classes/ActiveRecord/Base.html
There is a great convention, that when you create a gem, you do not create many top level constants. Instead, you should create a single module, which acts like a namespace for your gem. This is exactly what ActiveRecord module is.
ActiveRecord::Base is one of many components of ActiveRecord - others includes classes like Schema, SchemaDumper Relation, ImmutableRelation Validation, Scoping and many many more. You can see the whole list with ActiveRecord::Base. If creators of ActiveRecord put all of those constants in the main namespace, you would be largely limited in names you could give to your constants without having conflicts, hence the need of the wrapping module. This module also can contain some extra meta data, so at any point you can check the current version of the gem with ActiveRecord.version.

Having trouble chaining ActiveRecord::Associations record

I'm trying to retrieve an associated column named "contribution_amount" for each user but I'm getting undefined method error and I can't figure out why.
Controller has:
#payments = Payment.where(:contribution_date => Date.today).pluck(:user_id)
#users = User.where(:id => #payments).find_each do |user|
user.payments.contribution_amount
end
models have:
class User < ActiveRecord::Base
has_many :payments
end
class Payment < ActiveRecord::Base
belongs_to :user
end
Exact error in console is
`undefined method `contribution_amount' for #<ActiveRecord::Associations::CollectionProxy::ActiveRecord_Associations_CollectionProxy_Payment:0x007fb89b6b2c08>`
user.payments is a scope; that is, it represents a collection of Payment records. The contribution_amount method is only available on individual Payment records. You could say user.payments.first.contribution_amount, but I'm not sure that's your goal.
Are you trying to sum the contribution amounts? In that case, you'd want to use a method which aggregates collections of records: user.payments.sum(:contribution_amount).
Veering off-topic for a moment, it is generally better to push scoping methods down into your models. For example:
class User < ActiveRecord::Base
def self.with_payment_contribution_after(date)
joins(:payments).merge(Payment.with_contribution_after(date))
end
def self.with_contribution_amount
joins(:payments).group("users.id")
.select("users.*, sum(payments.contribution_amount) as contribution_amount")
end
end
class Payment < ActiveRecord::Base
def self.with_contribution_after(date)
where(:contribution_date => date)
end
end
# In your controller
#users = User.with_payment_contribution_after(Date.today)
.with_contribution_amount
# In a view somewhere
#users.first.contribution_amount
The advantages to structuring your code this way are:
Your scopes are not longer locked away in a controller method, so you can easily reuse them other places.
Your controller method can become simpler and more declarative. That is, it can express what information it wants, not how that information is acquired.
Breaking scopes down into smaller pieces implies that our code is better decomposed, and that which has been decomposed can be recomposed.
It's easier to test scopes via model unit tests then via controller testing.

Scoping a class method to current_user

I'm working on implementing a tagging system and I'm having problem querying for tagged objects with a scope.
For example, I would like to find all the user's items with a certain tag. With a class method I can currently find all the objects:
def self.tagged_with(name)
Tag.find_by_name(name).items
end
However, this has a problem. If I were to do something like: current_user.items.tagged_with(name) won't this existing method return ALL the items and not just items owned by the current_user? I suppose this is a simply querying issue but I can't figure out how to change a class method into something called on a collection. I have tried going the opposite way, to get a the collection through the tags, something like... tag.items.where(:user_id => current_user.id) but in this case, it's a many-to-many relationship and I haven't been able to get on thumb on this either.
What's the proper way to restrict a query like this?
Create an association on your User class that points to your Tag class.
class User < ActiveRecord::Base
has_many :tags
end
Then you can do:
current_user.tags.where(...)
If you don't already have an association in place, you'll need to create a migration to have the tags table reference your users table with a foreign key.
I think this will help you:
class Account < ActiveRecord::Base
has_many :people do
def find_or_create_by_name(name)
first_name, last_name = name.split(" ", 2)
find_or_create_by_first_name_and_last_name(first_name, last_name)
end
end
end
person = Account.first.people.find_or_create_by_name("David Heinemeier Hansson")
person.first_name # => "David"
person.last_name # => "Heinemeier Hansson"
So, basically you can define your method tagged_with directly into the association!
This example is took from the documentations ActiveRecord::Associations

Is there an ActiveRecord equivalent to using a nested subquery i.e. ... where NOT IN(select...)?

I have 3 models: Category, Account, and SubAccount
The relations are:
Accounts has_many :sub_accounts
Categories has_many :sub_accounts
I wanted to get a list of all Categories that are not used by a given account.
My method in the Category model currently looks like:
class Category < ActiveRecord::Base
def self.not_used_by(account)
Category.find_by_sql("select * from categories where id not in(select category_id from sub_accounts where account_id = #{account.id})")
end
end
My question is, is there a cleaner alternative than using SQL?
NB. I am currently using Rails 3(beta)
You could move the method to the account model and use more of ActiveRecord by doing something like:
class Account < ActiveRecord::Base
def unused_categories
Category.where("id NOT IN (?)", sub_accounts.map(&:category_id))
end
end
Then you could do something like:
Account.first.unused_categories
AR does not do this out of the box. You may also want to check the excellent SearchLogic gem for a programatic approach.
search = Category.search
search.id_not_in sub_accounts.map(&:category_id)
search.name_equals "auto"
search. ..# other conditions
search.all
Try MetaWhere. http://metautonomo.us/projects/metawhere
You'll need my fork of Arel installed until the changes get merged (soon!) but with it installed you can do something like:
Category.where(:id.not_in => sub_accounts.map(&:category_id))

Traversing HABTM relationships on ActiveRecord

I'm working on a project for my school on rails (don't worry this is not graded on code) and I'm looking for a clean way to traverse relationships in ActiveRecord.
I have ActiveRecord classes called Users, Groups and Assignments. Users and Groups have a HABTM relationship as well as Groups and Assignments. Now what I need is a User function get_group(aid) where "given a user, find its group given an assignment".
The easy route would be:
def get_group(aid)
group = nil
groups.each { |g| group = g if g.assignment.find(aid).id == aid }
return group
end
Is there a cleaner implementation that takes advantage of the HABTM relationship between Groups and Assignments rather than just iterating? One thing I've also tried is the :include option for find(), like this:
def get_group(aid)
user.groups.find(:first,
:include => :assignments,
:conditions => ["assignments.id = ?", aid])
end
But this doesn't seem to work. Any ideas?
First off, be careful. Since you are using has_and_belongs_to_many for both relationships, then there might be more than one Group for a given User and Assignment. So I'm going to implement a method that returns an array of Groups.
Second, the name of the method User#get_group that takes an assignment id is pretty misleading and un-Ruby-like.
Here is a clean way to get all of the common groups using Ruby's Array#&, the intersection operator. I gave the method a much more revealing name and put it on Group since it is returning Group instances. Note, however, that it loads Groups that are related to one but not the other:
class Group < ActiveRecord::Base
has_and_belongs_to_many :assignments
has_and_belongs_to_many :users
# Use the array intersection operator to find all groups associated with both the User and Assignment
# instances that were passed in
def self.find_all_by_user_and_assignment(user, assignment)
user.groups & assignment.groups
end
end
Then if you really needed a User#get_groups method, you could define it like this:
class User < ActiveRecord::Base
has_and_belongs_to_many :groups
def get_groups(assignment_id)
Group.find_all_by_user_and_assignment(self, Assignment.find(assignment_id))
end
end
Although I'd probably name it User#groups_by_assignment_id instead.
My Assignment model is simply:
class Assignment < ActiveRecord::Base
has_and_belongs_to_many :groups
end

Resources