Rails - apply an array of scopes - ruby-on-rails

I have a class that takes an array of scopes and applies them iteratively:
class AssignableLearningObjectives::Collector
def initialize(user:, only_self_assignable: false, scopes: [])
#user = user
#only_self_assignable = only_self_assignable
#scopes = scopes
end
.....
def available_objectives
objectives = assignable_objectives.or(manager_assigned_objectives).or(global_objectives).distinct
return objectives unless scopes.any?
scopes.each{ |scope| objectives = objectives.send(scope) }
objectives
end
My issue is with
scopes.each{ |scope| objectives = objectives.send(scope) }
objectives
Is there a better way of doing this? I was hoping for a rails method apply_scopes or something like that, however can't find anything like that.
My concern is the scopes are sent from the controller, and it is possible for the user to submit a request with a scope of 'destroy_all' or something equally fun.
Is there an easy way for me to let rails handle this? Or will I need to manually check each scope before I apply it to the collection?
Thanks in advance
EDIT:
I'm happy to validate each scope individually if I have to, but even that's causing issues. There is a method in rails which was dropped in 3.0.9 which I could use, Model.scopes :
https://apidock.com/rails/v3.0.9/ActiveRecord/NamedScope/ClassMethods/scopes
however that's deprecated. Is there any method I can call on a class to list its scopes? I can't believe the feature was there in rails 3 and removed completely...

From the fine guide:
14 Scopes
[...]
To define a simple scope, we use the scope method inside the class, passing the query that we'd like to run when this scope is called:
class Article < ApplicationRecord
scope :published, -> { where(published: true) }
end
This is exactly the same as defining a class method, and which you use is a matter of personal preference:
class Article < ApplicationRecord
def self.published
where(published: true)
end
end
So scope is mostly just a fancy way of creating a class method that is supposed to have certain behavior (i.e. return a relation) and any class method method that returns a relation is a scope. scope used to be something special but now they're just class methods and all the class methods are copied to relations to support chaining.
There is no way to know if method Model.m is a "real" scope that will return a relation or some random class method without running it and checking what it returns or manually examining its source code. The scopes method you seek is gone and will never come back.
You could try to blacklist every class method that you know is bad; this way lies bugs and madness.
The only sane option is to whitelist every class method that you know is good and is something that you want users to be able to call. Then you should filter the scopes array up in the controller and inside AssignableLearningObjectives::Collector. I'd check in both places because you could have different criteria for what is allowed depending on what information is available and what path you're taking through the code; slightly less DRY I suppose but efficiency and robustness aren't friends.
You could apply the scope whitelist in the AssignableLearningObjectives::Collector constructor or in available_objectives.
If you want something prettier than:
scopes.each{ |scope| objectives = objectives.send(scope) }
objectives
then you could use inject:
def available_objectives
objectives = assignable_objectives....
scopes.inject(objectives) { |objectives, scope| objectives.send(scope) }
end

Related

Rails active record model method chaining and getting the initial input

This feels like it should be very simple but am unsure the correct syntax. This is me playing and learning, so this isn't pressing.
I would like to write a custom method on a model that performs an action and takes the input from the method chain instead of require you to set the input arguments in the method call.
Given that we have the following simple class;
class Comment < ApplicationRecord
scope :for_user, ->(u) { where(user_id: u.id) }
def self.do_thing
# How do I get the results from the chain?
# For example how do I get the IDs?
end
end
So I'd like to be able to do this;
Comments.for_user(current_user).do_thing
And have the do_thing method know what the results of Comments.for_user(current_user)
Now obviously I could make do_thing have method arguments and just go that route, but I'm playing and learning about method chaining..
Thanks for your input.
Class Comment < ApplicationRecord
scope :for_user, ->(u) { where(user_id: u.id) }
def self.do_thing
# How do I get the results from the chain?
# For example how do I get the IDs?
self.ids
end
end
Use self. The way scopes work is that they return an ActiveRecord::Relation object that proxies method calls back to the model class.
Although in this case the method will actually break chaining since it returns an array and not self or an ActiveRecord::Relation.

Rails model class method for collection of objects

I'm having trouble writing class methods to use on collections of ActiveRecord objects. I've run into this issue twice in the last couple of hours, and it seems like a simple problem, so I know I'm missing something, but I haven't been able to find answers elsewhere.
Example:
class Order < ActiveRecord::Base
belongs_to :customer
scope :month, -> { where('order_date > ?', DateTime.now.beginning_of_month.utc) }
def self.first_order_count
map(&:first_for_customer?).count(true)
end
def first_for_customer?
self == customer.orders.first
# this self == bit seems awkward, but that's a separate question...
end
end
If I call Order.month.first_order_count, I get
NoMethodError: undefined method 'map' for #<Class:...
As far as I know, that's because map can't be called directly on Order, but needs an Enumerable object instead. If I call Order.year.map(&:first_for_customer?).count(true), I get the desired result.
What's the right way to write methods to use on a collection of ActiveRecord objects, but not on the class directly?
In your case, you can use a trick in this case.
def self.first_order_count
all.map(&:first_for_customer?).count(true)
end
Will do the trick, without any other problems, this way if you concatenate this method on where clause you still get results from that where, this way you get what you need if you call this method directly on Order.
ActiveRecord collections are usually manipulated using scopes, with the benefits of being able to chain them and let the database do the heavy lifting. If you must manage it in Ruby, you can start with all.
def self.first_order_count
all.map(&:first_for_customer?).count(true)
end
What are you trying to achieve with your code though?

Where to put method that returns a collection

I have a Shop model.
In my class, I have created several methods, that allow me to make my code easier to read.
Things like :
# Returns True if the shop has an owner
def is_claimed
return self.owner_id != nil
end
Now I want to create a method that would return a collection of Shops.
Something like :
def get_all_open
return Shop.where("shops.closed != 1")
end
I am simply wondering where I should place it, so it is available from anywhere where I can potentially use
Shop.all
in my code.
If I place it in the Model, it will be available to instances of shops, which would sound weird.
This method also belongs to Shop, so I don't really like placing it in the ApplicationController.
I also thought about placing it in the shops_controller, but then it won't be available easily to other controllers.
I have looked on the internet, but failed to find something I like so far. What would be considered a good practice in rails ?
Thanks
You can keep it in the Model, but instead of defining it as an instance method, define it as a class method:
def self.all_open
where("shops.closed != 1")
end
This way you can use it like this:
Shop.all_open
And even nest methods, like this:
Shop.all_open.where(field: "value")
UPDATE:
Like others pointed out, you could also use scopes (this is the preferred way):
class Shop < ActiveRecord::Base
scope :all_open, -> { where("shops.closed != 1") }
end
The usage would be the same:
Shop.all_open
This is what Scopes are usually used for.
class Shop < ActiveRecord::Base
scope :open, ->{ where("shops.closed != 1") }
# ->{ where.not(closed: true) } # <- Either will work, but I prefer this
# ...
end
Then:
Shop.open.all # => (a collection of Shops)
The advantage of scopes is that you can chain them, so, for example, if your Shop model had a type attribute, you could get all open Shops with a certain type like so:
Shop.open.where(type: "pharmacy").all
Define it as a class method in Shop.
class Shop
def self.all_open
where("shops.closed != 1")
end
end
open_shops = Shop.all_open
Note:
0) The self in def self.all_open is what makes this a class method, defined on the class Shop itself, and callable on the class itself as Shop.all_open, instead of an ordinary instance method which needs to be called on a particular instance.
1) Calling the method all_open instead of get_all_open leads to more natural and idiomatic code when you call Shop.all_open. I might in fact just call it open, leading to open_shops = Shop.open.
2) Note the suggested implementation omits the return, just for readability. Note it also omits the Shop.where, and just does where. This mostly only matters if you end up making sub-classes of Shop, which you probably won't, but is good style, and shorter code. Just where is the same as self.where, and since this is a class method, self is the class itself, Shop (unless you have sub-classes inheriting this code, in which case self could be a sub-class).
3) You can in fact chain this with toher stuff, no problem. Check it out: Shop.all_open.where(:city => "Baltimore").order(:updated_at). Pretty neat, it just works!
4) Some people will tell you to use ActiveRecord 'scopes' for this. You don't really need to for this case, you can just write a plain old method, just like that. The Rails docs acknowledge this about using a scope for this sort of thing: "This is exactly the same as defining a class method, and which you use is a matter of personal preference." I'd just use a plain old method here, just like you started out with (except defined as a class method on the model) -- it's more straightforward, it does what you expect, you don't need to learn about 'scopes', it's just a method. Using a scope to do this instead is kind of leftover from before Rails supported using an ordinary class method in this way, an ordinary class method is just fine.

Filtering Rails Controllers Actions from Query Params

I'm looking for the best method for filtering the records I return to a view in Rails, based on an arbitrary amount of query parameters, e.g:
http://localhost:3000/foo?some_field=fubar&this_field_left_blank=&a_third_field=bar
What I'm currently doing
I have found and used the has_scope gem, which worked rather nicely, but my work colleagues have expressed trepidation that scopes declare class methods of the same name as the fields on the class, e.g:
scope :some_field, -> f { where some_field: f }
where my class looks something like
class Foo
....
attribute :some_field
....
I have also considered building a 'chain' of .where clauses by way of a large switch statement, but that seems ugly.
tl;dr
Is there a pattern for filtering records based on query parameters in Rails, and/or a best practice?
My eventual solution
After reading the answer given by Andrew Wei and spending a little more time with the problem, I decided to use a loop over the parameters with responds_to on the class itself, like so:
params.each do |params,value|
#mymodels = MyModel.send "by_#{filter}", value, #mymodels if MyModel.respond_to? "by_#{filter}" and not value.blank?
end
where each of the MyModel class methods looks something like:
def self.by_some_field(value, relation)
relation.where some_field: value
end
This is what I usually do. I'm not saying this is the "Rails way".
where = {}
if params[:one]
where["one"] = params[:one]
end
if params[:two] # .empty? , .nil?, x > 0... etc
where["two"] = params[:two].to_i
end
#somethings = Something.where(where)
Obviously you would protect how your variables are assigned. I'm just trying to demonstrate the pattern I use.

Tracking model changes in Rails, automatically

In my rails app I would like to track who changes my model and update a field on the model's table to reflect.
So, for example we have:
class Foo < ActiveRecord::Base
before_create :set_creator
belongs_to :creator, :class_name => "User"
protected
def set_creator
# no access to session[:user_id] here...
end
end
What's a good testable way for me to get at the user_id from my model? Should I be wacking this data in Thread.current ?
Is it a better practice to hand this information from the controller?
Best practice in MVC is to have your Models be stateless, the controller gets to handle state. If you want the information to get to your models, you need to pass it from the controller. Using a creation hook here isn't really the right way to go, because you are trying to add stateful data, and those hooks are really for stateless behavior.
You can pass the info in from the controller:
Foo.new(params[:foo].merge {:creator_id => current_user.id})
Or you can create methods on User to handle these operations:
class User
def create_foo(params)
Foo.new(params.merge! {:creator_id => self.id})
end
end
If you find yourself writing a lot of permissions code in the controller, I'd go with option 2, since it will let you refactor that code to the model. Otherwise option 1 is cleaner.
Omar points out that it's trickier to automate, but it can still be done. Here's one way, using the create_something instance method on user:
def method_missing(method_sym, *arguments, &block)
meth = method_sym.to_s
if meth[0..6] == "create_"
obj = meth[7..-1].classify.constantize.new(*arguments)
obj.creator_id = self.id
else
super
end
end
You could also override the constructor to require user_ids on construction, or create a method inside ApplicationController that wraps new.
There's probably a more elegant way to do things, but I definitely don't like trying to read state from inside Model code, it breaks MVC encapsulation. I much prefer to pass it in explicitly, one way or another.
Yeah, something like that would work, or having a class variable on your User model
cattr_accessor :current_user
Then in your controller you could have something like:
User.current_user = current_user
inside a before filter (assuming current_user is the logged in user).
You could then extend AR:Base's create/update methods to check for the existence of a created_by/updated_by field on models and set the value to User.current_user.
I'd create new save, update, etc methods that take the user_id from everything that calls them (mainly the controller).
I'd probably extend ActiveRecord:Base into a new class that handles this for all the models that need this behaviour.
I wouldn't trust Thread.current, seems a bit hackish. I would always call a custom method which takes an argument:
def create_with_creator(creator, attributes={})
r = new(attributes)
r.creator = creator
r.save
end
As it follows the MVC pattern. The obviously inherient problem with this is that you're going to be calling create_with_creator everywhere.
You might find PaperTrail useful.
Probably you could check out usertamp plugins, found two in github
http://github.com/delynn/userstamp/tree/master
http://github.com/jnunemaker/user_stamp/tree/master

Resources