Rails: Using service class methods in a scope query - ruby-on-rails

I have a user model.
I'm trying to set my user index view to show only those users who have completed an onboarding process.
My approach to doing that is:
index:
<% Users.onboarded.each do |user| %>
In my user.rb, I tried to define a scope, for onboarding, as:
scope :onboarded, -> { where (:user_is_matchable?) }
I have a method in my organisation service class which has:
class UserOrganisationMapperService
attr_accessor :user
private
def user_is_matchable?
profile.present? && matching_organisation.present?
end
When i try this, I get an error that says:
undefined method `onboarded' for Users:Module
Can anyone see where I've gone wrong?

Firstly: Users or User in your user.rb file.. what's the actual name of the class? because it should be User not Users
Secondly: scope :onboarded, -> { where (:user_is_matchable?) } this is just not going to work. a scope is an ActiveRecord query - it can only deal with details of the actual structure in the database. if you don't have a column in your users table called user_is_matchable? then this scope will complain and not work.
you need to make that scope into something that would work just on the database.
I'm only guessing (you haven't given us the full structure of your relations here) but would you be able to do something like:
scope :onboarded, -> { where ("profile_id IS NOT NULL AND matching_organisation_id IS NOT NULL") }
???
Alternatively, you'll need to make it run just in ruby - which will be slower (especially if your table gets very big, as user-tables are wont to do). But if you're ok with it being super slow, then you could do this:
class User < ActiveRecord::Base
def self.onboarded
all.select(&:user_is_matchable?)
end
or something similar...

Related

Call boolean instance method as a 'scope'

I have a model
Email
and an instance method
def sent_to_user?(user)
self.who_to == user
end
where who_to is another instance method doing some complicated stuff to check.
There's a lot more stuff going on in the background there so I can't easily turn it into an activerecord query.
I want to do something like:
scope :sent_to_user, -> (user) { sent_to_user?(user)}
#user.emails.sent_to_user
and return a only those emails that return true for 'sent_to_user?'
Have tried
scope :sent_to_user, -> (user) { if sent_to_user?(user)}
....etc.
Not quite sure how to build that scope / class method
You can't (at least shouldn't) use scopes this way. Scopes are for returning ActiveRecord relations onto which additional scopes can be chained. If you want to use a scope, you should produce the necessary SQL to perform the filtering in the database.
If you want to filter results in Ruby and return an array, you should use a class-level method, not a scope:
class Email < ActiveRecord::Base
def self.sent_to_user(user)
select { |record| record.sent_to_user?(user) }
end
end
You should write who_to in ActiveRecord logic, like
scope :sent_to_user, -> (user) { joins(:recipient)where(recipient_id: user.id)}
Assuming that user has_many :emails you can do something like:
class User < ActiveRecord::Base
has_many :emails
# ...
def emails_sent_to_user
emails.select { |e| e.sent_to_user?(self) }
end
end
Funnily enough, I worked out a method to do the checking which is a bit of a hack but works in this case, might be useful to somebody.
The issue with the accepted solution, though it's certainly the only way to do it based on the restrictions I described, is that performance can be seriously sluggish.
I'm now using:
scope :sent_to_user, -> (user) {"json_email -> 'to' ILIKE ?", "%#{user.email)}%"}
Where "json_email" is an email object parsed into json (it's the way we store them in the db). This cut out the need to use the Mail gem and increased performance dramatically.

Specify currently grabbed records within Model class method

I have a class method where I want to modify the records that are currently grabbed by an ActiveRecord::Relation object. But I don't know how to refer to the current scope in a class method. self does not do it.
Example:
class User < ActiveRecord::Base
...
def self.modify_those_records
#thought implicitly #to_a would be called on currently grabbed records but doesn't work
temp_users_to_a = to_a
...
end
end
I would use it like this:
User.some_scope.modify_those_records
So User.some_scope would return to me an ActiveRecord::Relation that contains a bunch of User records. I then want to modify those records within that class method and then return them.
Problem is: I don't know how to explicitly refer to "that group of records" within a class method.
You can use current_scope:
def self.modify_those_records
current_scope.each do |user|
user.do_something!
end
end
If you want to order Users based on their admin rights, you would be better to use ActiveRecord:
scope :order_admins_first, order('CASE WHEN is_admin = true THEN 0 ELSE 1 END, id')
User.some_scope.order_admins_first
This code implies that you have a boolean column is_admin on the users table.
I would argue that a combination of a scope with each and an instance method is easier to understand than a class method. And as a bonus it is easier to test, because you can test all steps in isolation:
Therefore instead of User.some_scope.modify_those_records I would do something like:
User.some_scope.each(&:modify)
and implement a instance method:
# in user.rb
def modify
# whatever needs to be done
end
If you only want to modify the order of the records - better way is to add a sort field (if you do not have it already) to the model and sort by that.
User.some_scope.order(name: :asc).order(is_admin: :desc)

Undefined method error for scope on STI subclass

The Setup
I have an STI setup like so:
class Transaction < ActiveRecord::Base
belongs_to :account
scope :deposits, -> { where type: Deposit }
end
class Deposit < Transaction
scope :pending, -> { where state: :pending }
end
class Account < ActiveRecord::Base
has_many :transactions
end
If I call:
> a = Account.first
> a.transactions.deposits
...then I get what I expect, a collection of Deposit instances, however if I look at the class of what's returned:
> a.transactions.deposits.class
...then it's actually not a Deposit collection, it's still a Transaction collection, ie. it's a Transaction::ActiveRecord_AssociationRelation
The Problem
So, to the problem, if I then want to call one of the Deposit scopes on that collection it fails:
> a.transactions.deposits.pending
NoMethodError: undefined method `pending' for #<Transaction::ActiveRecord_Associations_CollectionProxy:0x007f8ac1252d00>
Things I've Checked
I've tried changing the scope to Deposit.where... which had no effect, and also to Deposit.unscoped.where... which actually returns the right collection object, but it strips all the scope, so I lose the account_id=123 part of the query so it fails on that side.
I've checked this and the problem exists for both Rails 4.1 and 4.2. Thanks for any pointers on how to make this work.
I know there's a workaround, but...
I know I could work around the issue by adding a has_many :deposits into Account, but I'm trying to avoid that (in reality I have many associated tables and many different transaction subclasses, and I'm trying to avoid adding the dozens of extra associations that would require).
Question
How can I get what's returned by the deposits scope to actually be a Deposit::ActiveRecord_Association... so that I can chain my scopes from Deposit class?
I created an isolated test for your issue here:https://gist.github.com/aalvarado/4ce836699d0ffb8b3782#file-sti_scope-rb and it has the error you mentioned.
I came across this post from pivotal http://pivotallabs.com/merging-scopes-with-sti-models/ about using were_values in a scope to get all the conditions. I then used them on unscope to force the expected class, basically this:
def self.deposits
conditions = where(nil).where_values.reduce(&:and)
Deposit.unscoped.where(conditions)
end
This test asserts that it returns a Deposit::ActiveRecord_Relation https://gist.github.com/aalvarado/4ce836699d0ffb8b3782#file-sti_scope2-rb
Update
You can also write this as a scope if you prefer:
scope :deposits, -> { Deposit.unscoped.where where(nil).where_values.reduce &:and }
As a quick workaround you can do > a.transactions.deposits.merge(Deposit.pending), but can't think of a different way of solving it. I'll think and try more options later and come back if I find anything.
You might want to say that an Account has_many :deposits
class Account < ActiveRecord::Base
has_many :transactions
has_many :deposits
end
Then you should be able to query
a.deposits.pending

How to make default_scope in Rails model dynamic?

In one of my Rails models I have this:
class Project < ActiveRecord::Base
belongs_to :user
default_scope order("number ASC")
end
Now the problem is that I want each user to be able to set his or her default_scope individually. For example, a user A might want default_scope order("date ASC"), another one might want default_scope order("number DESC").
In my User table I even have columns to store these values: order_column and order_direction.
But how can I make the default_scope in the model dynamic?
Thanks for any help.
As #screenmutt said, default scopes are not meant to be data-driven, they are meant to be model driven. Since this scope is going to change according to each user's data I'd use a regular scope for this.
#fmendez answer is pretty good but it uses default scope which I just explained why it is not recommended using this method.
This is what I'd do in your case:
class Post < ActiveRecord::Base
scope :user_order, lambda { order("#{current_user.order_column} #{current_user.order_direction}")}
end
Also a very important thing to notice here is SQL injection: Since you are embedding current_user.order_column and current_user.order_direction inside your query, you MUST ensure that the user can only feed these columns into the database with valid data. Otherwise, users will be able to craft unwanted SQL queries.
You won't want to use default_scope. What you do what is regular scope.
class Post < ActiveRecord::Base
scope :created_before, ->(time) { where("created_at < ?", time) }
end
Scope | Ruby on Rails
You could do something like this:
def self.default_scope
order("#{current_user.order_column} #{current_user.order_direction}")
end
This should dynamically pick the values stored in the current_user's order_column and order_direction columns.
You can define a class method with whatever logic you require and set your default scope to that. A class method is identical to a named scope when it returns a relation,eg by returning the result of a method like order.
For example:
def self.user_ordering
# user ording logic here
end
default_scope :user_ordering
You may want to add a current_user and current_user= class methods to your User model which maintains the request user in a thread local variable. You would typically set the current user on your User model from your application controller. This makes current_user available to all your models for logic such as your sorting order and does it in a thread safe manner.

Override mongomapper has_many association

I use Rails 3 with MongoMapper.
I want to add some records to the result of has many association.
For example, user has_many posts
class User
include MongoMapper::Document
many :posts
end
By default it will show only posts which belongs to the user, but if he/she specify special option in query (or in the user's settings menu, say show-commented=true), then I also need to add posts where user left any comments. So I think to override posts method
def posts
super + (show_commented_posts ? commented_posts : [])
end
But of course it doesn't work. How can I correctly override this method using mongo_mapper? Or is there any better approach for that problem?
Overriding methods on mongomapper is a very bad idea, you should try to refrain from doing it as it creates a lot of problems that are hard to trace back (I've been burned before by this).
Instead, you should consider using a scope such as
class Post
scope :related_to_user, lambda {|user| where('$or' => [ {user_id: user.id}, {'comments.user_id' => user.id}]) }
end
Then you can call
Post.related_to_user(current_user)

Resources