How to handle pagination with Pundit? - ruby-on-rails

I've just started using the Pundit gem for authorisation in a Rails 4 app.
Everything's going fine but I can get my head around how pagination will work in the index action.
My controller's index action looks like:
def index
#records = policy_scope(Record)
end
The Scope class within my RecordPolicy then goes:
class Scope < Struct.new(:user, :scope)
def resolve
if user.has_role? :admin
# get all records
else
# get user specific records
end
end
end
This all works fine. I'd like to know how I would handle pagination though. Naturally this involves passing in a page parameter etc and I'm not sure how to do this without subsclassing the Scope class.

The policy_scope(Record) method returns an ActiveRecord::Relation object, then you can chain the pagination method, depending on which gem you use (will_paginate, kaminari).
def index
#records = policy_scope(Record).paginate(params[:page])
end

For googlers. The answer above is technically correct, but with most recent versions of Kaminari (mine is 0.17), the method to chain is page(params[:page])

Related

Rails N+1 query : monkeypatching ActiveRecord::Relation#as_json

Situation
I have a model User:
def User
has_many :cars
def cars_count
cars.count
end
def as_json options = {}
super options.merge(methods: [:cars_count])
end
end
Problem
When I need to render to json a collection of users, I end up being exposed to the N+1 query problem. It is my understanding that including cars doesn't solve the problem for me.
Attempted Fix
What I would like to do is add a method to User:
def User
...
def self.as_json options = {}
cars_counts = Car.group(:user_id).count
self.map do |user|
user.define_singleton_method(:cars_count) do
cars_counts[user.id]
end
user.as_json options
end
end
end
That way all cars counts would be queried in a single query.
Remaining Issue
ActiveRecord::Relation already has a as_json method and therefore doesn't pick the class defined one. How can I make ActiveRecord::Relation use the as_json method from the class when it is defined? Is there a better way to do this?
Edits
1. Caching
I can cache my cars_count method:
def cars_count
Rails.cache.fetch("#{cache_key}/cars_count") do
cars.count
end
end
This is nice once the cache is warm, but if a lot of users are updated at the same time, it can cause request timeouts because a lot of queries have to be updated in a single request.
2. Dedicated method
Instead of calling my method as_json, I can call it my_dedicated_as_json_method and each time I need to render a collection of users, instead of
render json: users
write
render json: users.my_dedicated_as_json_method
However, I don't like this way of doing. I may forget to call this method somewhere, someone else might forget to call it, and I'm losing clarity of the code. Monkey patching seems a better route for these reasons.
Have you considered using a counter_cache for cars_count? It's a good fit for what you're wanting to do.
This blog article also offers up some other alternatives, e.g. if you want to manually build a hash.
If you really wanted to continue down the monkey patching route, then ensure that you are patching ActiveRecord::Relation rather than User, and override the instance method rather than creating a class method. Note that this will then affect every ActiveRecord::Relation, but you can use #klass to add a condition that only runs your logic for User
# Just an illustrative example - don't actually monkey patch this way
# use `ActiveSupport::Concern` instead and include the extension
class ActiveRecord::Relation
def as_json(options = nil)
puts #klass
end
end
Option 1
In your user model:
def get_cars_count
self.cars.count
end
And in your controller:
User.all.as_json(method: :get_cars_count)
Option 2
You can create a method which will get all the users and their car count. And then you can call the as_json method on that.
It would roughly look like:
#In Users Model:
def self.users_with_cars
User.left_outer_joins(:cars).group(users: {:id, :name}).select('users.id, users.name, COUNT(cars.id) as cars_count')
# OR may be something like this
User.all(:joins => :cars, :select => "users.*, count(cars.id) as cars_count", :group => "users.id")
end
And in your controller you can call as_json:
User.users_with_cars.as_json
Here is my solution in case someone else is interested.
# config/application.rb
config.autoload_paths += %W(#{config.root}/lib)
# config/initializers/core_extensions.rb
require 'core_extensions/active_record/relation/serialization'
ActiveRecord::Relation.include CoreExtensions::ActiveRecord::Relation::Serialization
# lib/core_extensions/active_record/relation/serialization.rb
require 'active_support/concern'
module CoreExtensions
module ActiveRecord
module Relation
module Serialization
extend ActiveSupport::Concern
included do
old_as_json = instance_method(:as_json)
define_method(:as_json) do |options = {}|
if #klass.respond_to? :collection_as_json
scoping do
#klass.collection_as_json options
end
else
old_as_json.bind(self).(options)
end
end
end
end
end
end
end
# app/models/user.rb
def User
...
def self.collection_as_json options = {}
cars_counts = Car.group(:user_id).count
self.map do |user|
user.define_singleton_method(:cars_count) do
cars_counts[user.id]
end
user.as_json options
end
end
end
Thanks #gwcodes for pointing me at ActiveSupport::Concern.

Is it possible to use single method to authorise multiple controller action in Rails Pundit?

I am created new rails application and I want to restrict user actions based on only one condition like record can be editable by owner(created_by) and sub-owner(Added by owner). I have models like App, User and controller like AppController. In AppController I have more than one actions like index, create, show, update, delete. I have one policy like AppPolicy. Here I need to create only one method to verify all actions but by default each action requires another method like action_name? in policy class.
Example
Existing code:
class AppPolicy < ApplicationPolicy
class Scope < Scope
def resolve
scope
end
end
def action1?
record.users.include? (user)
end
def action2?
record.users.include? (user)
end
def action3?
record.users.include? (user)
end
end
From above code we can see a same condition reside in all methods. I need to use only one method to verify action1, action2, action3. I don't know this is possible or not in Pundit.
I know this is an old question but I just had the same problem.
I can think about 2 solutions:
solution 1
When you know all the actions that could be called.
You can use define_method, like this
[:action1?, :action2?].each do |m|
define_method(m) { record.users.include? (user) }
end
solution 2
When you don't know all the actions. (this could be dangerous)
You can use a combination of method_missing and respond_to_missing. The latter is needed since pundit will call internally respond_to before calling the corresponding method of the policy.
Example:
def method_missing(m, *args, &block)
record.users.include? (user)
end
def respond_to_missing?(method_name, include_private = false)
true #Here it would be better to add some conditions
end
You can use cancan (or cancancan) gem rubygems link
You can create the ability configuration file with
rails g cancan:ability
The authorize! method in your controller will raise an exception if the user is not able to perform the given action, so call it on before_action callback.
Documentation here

Can't use `find_by` method while `where` works in ActiveRecord::Base

I have a method like this, that works fine.
class User < ActiveRecord::Base
def self.find_or_create_from_twitter_id(twitter_id)
user = where(twitter_id: twitter_id).first
user ||= create_from_twitter_id(twitter_id)
end
end
Then I changed where(...).first to find_by, because I thought the two expression are basically same.
def self.find_or_create_from_twitter_id(twitter_id)
user = find_by(twitter_id: twitter_id)
user ||= create_from_twitter_id(twitter_id)
end
But I get undefined method `find_by' for #<Class:0x007fbb2b674970> error when I try to create a User.
I have no idea why find_by doesn't work here. I'll be very grateful if you tell me what is wrong.
find_by method is introduced in Rails 4. If you're using Rails 3.x or lesser version then use: find_by_<attribute_name> instead:
find_by_twitter_id(twitter_id)
But, then there's another method which find and create by the attributes passed to it, which you can use if it fits your needs:
find_or_create_by_twitter_id(twitter_id)

Find Children of Specific Page with RefineryCMS and ActiveRecord

Right now I have a page with a custom template.
I would like to use a decorator before filter like so in the pages decorator:
before_filter :get_gallery_index, :only => [:gallery_main]
To define this method I would like to declare something similar to:
def get_gallery_index
# a = Refinery::Page.where(id: 6).children <- would be ideal
end
Of course, children is not a valid method with Refinery Pages and ActiveRecord. Does anyone have a suggestion of how I can get these records?
Got it by searching through parent_id:
def get_gallery_index
#gallery_index = Refinery::Page.where(:parent_id => 6)
end

How to always set a value for account-scope in Rails?

I'm working on a multi-user, multi-account App where 1 account can have n users. It is very important that every user can only access info from its account. My approach is to add an account_id to every model in the DB and than add a filter in every controller to only select objects with the current account_id. I will use the authorization plugin.
Is this approach a good idea?
What is the best way to always set the account_id for every object that is created without writing
object.account = #current_account
in every CREATE action? Maybe a filter?
Also I'm not sure about the best way to implement the filter for the select options. I need something like a general condition: No matter what else appears in the SQL statement, there is always a "WHERE account_id = XY".
Thanks for your help!
This is similar to a User.has_many :emails scenario. You don't want the user to see other peoples emails by changing the ID in the URL, so you do this:
#emails = current_user.emails
In your case, you can probably do something like this:
class ApplicationController < ActionController::Base
def current_account
#current_account ||= current_user && current_user.account
end
end
# In an imagined ProjectsController
#projects = current_account.projects
#project = current_account.projects.find(params[:id])
I know, I know, if you access Session-variables or Instance variables in your Model you didn't understand the MVC pattern and "should go back to PHP". But still, this could be very useful if you have - like us - a lot of controllers and actions where you don't always want to write #current_account.object.do_something (not very DRY).
The solution I found is very easy:
Step 1:
Add your current_account to Thread.current, so for example
class ApplicationController < ActionController::Base
before_filter :get_current_account
protected
def get_current_account
# somehow get the current account, depends on your approach
Thread.current[:account] = #account
end
end
Step 2:
Add a current_account method to all your models
#/lib/ar_current_account.rb
ActiveRecord::Base.class_eval do
def self.current_account
Thread.current[:account]
end
end
Step 3: Voilá, in your Models you can do something like this:
class MyModel < ActiveRecord::Base
belongs_to :account
# Set the default values
def initialize(params = nil)
super
self.account_id ||= current_account.id
end
end
You could also work with something like the before_validation callback in active_record and then make with a validation sure the account is always set.
The same approach could be used if you always want to add the current_user to every created object.
What do you think?
To answer your second question, check out the new default_scope feature in Rails 2.3.
I understand that you don't want to bother about scoping you account all time. Lets be honest, it's a pain in the a**.
To add a bit magic and have this scoping done seamlessly give a look at the following gem
http://gemcutter.org/gems/account_scopper
Hope this helps,
--
Sebastien Grosjean - ZenCocoon

Resources