Call boolean instance method as a 'scope' - ruby-on-rails

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.

Related

Rails: Using service class methods in a scope query

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...

Rails sorting gem

I'd like to create a gem that modifies ActiveRecord::Base select methods. For instance, when I include my gem and type:
Somemodel.all
it should return an array ordered by id as normally but in descending order instead of ascending. I have no clue how it should look. I would not like to create additional methods like:
Somemodel.where(name: "John").revert_it
but simply do:
Somemodel.where(name: "John")
I was wondering about modifying ActiveRecord::Base methods, but it does not make any sense. IMO the best way is to callback after every ActiveRecord::Base method that will do it.
You can use the ActiveRecord method default_scope to achieve this:
class MyModel < ActiveRecord::Base
default_scope { order("id DESC") }
end
MyModel.all
# => SELECT * FROM my_models ORDER BY id DESC
It's not advisable to modify any core ActiveRecord methods (unless you have a really good reason), because that will make lot of confusion in future.
Even if you are thinking of modifying the select , you have make sure that you always return an ActiveRecord relation , so that it can be chained with as the standard way,
For your example, AR already has a method
User.all.reverse
I got fully valid answer:
def self.included(base)
base.class_eval do
default_scope { order(id: 'desc') }
end
end
Above method should be inside some module and then module included within some model class

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.

different default_scopes for searching and saving in rails (or a better implementation than the one I have)?

I have a rails app with 2 types of users, authenticated and unauthenticated (separated by a email_authenticated:boolean in the database). when I create a user I want it to be unauthenticated but every time I perform any function I want to perform that upon the authenticated list by default. I initially tried to do this by providing a default_scope but found out that this modifies the way the record is saved overrides the default (e.g. the default turns to true rather than false in the example)
# email_authenticated :boolean default(FALSE), not null
class User < ActiveRecord::Base
default_scope { where(email_authenticated: true) }
scope :authenticated, ->{ where(email_authenticated: true) }
scope :unauthenticated, ->{unscoped.where(email_authenticated: false)}
end
does anyone have a suggestion for a way to have a scope only apply on searches, or a smarter way of achieving what I'm going for. I don't want to have to call User.authenticated every time I search if I remove the default scope, similarly I don't want to call User.unauthenticated every time I save on the other hand.
Kind of seems like a hack, but you can do:
class User < ActiveRecord::Base
default_scope { where("email_authenticated = ?", true) }
end
Documented here: http://apidock.com/rails/ActiveRecord/Base/default_scope/class. I just tested it and it works, without the side effect on create.

Scope for multiple models

I have several objects that all have an approved field.
What would be the best way to implement a scope to use across all models?
For example, I have a sighting object and a comment object. They both have to be approved by an admin before being availalbe to the public.
So how can I create a scope that returns comment.approved as well as sighting.approved respectively without repeating it on each model? Is this where concerns comes into play?
While just declaring a scope in each model is fine if you just want the scoping functionality. Using an ActiveSupport::Concern will give you the ability to add additional methods as well if that's something you think is going to happen. Here's an example:
# /app/models/concerns/approved.rb
module Approved
extend ActiveSupport::Concern
included do
default_scope { where(approved: false) }
scope :approved, -> { where(approved: true) }
end
def unapprove
update_attribute :approved, false
end
end
class Sighting < ActiveRecord::Base
include Approved
end
class Comment < ActiveRecord::Base
include Approved
end
Then you can make calls like Sighting.approved and Comment.approved to get the appropriate list of approved records. You also get the unapprove method and can do something like Comment.approved.first.unapprove.
In this example, I've also included default_scope which will mean that calls like Sighting.all or Comment.all will return only unapproved items. I included this just as an example, it may not be applicable to your implementation.
Although I've noticed the scope pulled from the concerns needs to be the last scope when concatenating scopes. I'm not quite sure why.
Comment.existing.approved
When I tried it as:
Comment.approved.existing
It failed silently.
And I take that back. I was migrating older code and using scopes with conditions instead of lambdas. When I replaced :conditions the scope order no longer mattered.
scope :existing, -> { where("real = 1") }
replaced
scope :existing, :conditions => "real = 1"

Resources