How to convert a method into a scope. - ruby-on-rails

Hello I am trying to convert the method self.liked_by(user) into a scope. I am not entirely sure what my instructor is asking for so any interpretations on the question are greatly appreciated.
this is the method in question that I am supposed to turn into a scope.
def self.liked_by(user)
joins(:likes).where(likes: { user_id: user.id })
end
this is where the method appears in the model
class Bookmark < ActiveRecord::Base
belongs_to :user
belongs_to :topic
has_many :likes, dependent: :destroy
before_validation :httpset
validates :url, format: { with: /\Ahttp:\/\/.*(com|org|net|gov)/i,
message: "only allows valid URLs." }
def self.liked_by(user)
joins(:likes).where(likes: { user_id: user.id })
end
def httpset
if self.url =~ /\Ahttp:\/\/|\Ahttps:\/\//i
else
if self.url.present?
self.url = "http://"+ self.url
else
self.url = nil
end
end
end
end
And this is where the method is called in the controller
class UsersController < ApplicationController
def show
user = User.find(params[:id])
#bookmarks = user.bookmarks
#liked_bookmarks = Bookmark.liked_by(user)
end
end
Thanks for looking at my problem and have a good day.

#liked_bookmarks = Bookmark.liked_by(user)
In this line, in the same way you send the user parameter to a method, the same way you can send it to a scope.
class Bookmark < ActiveRecord::Base
---------
---------
scope :liked_by, ->(user) { joins(:likes).where(likes: { user_id: user.id }) }
---------
---------
end
the parameter you sent from the scope call can be accessed using the (user{or any name) in the scope
reference of scopes

As Owen suggested, read the docs to understand what scopes are. It is just another syntax to define your model's class methods (just like the one you already have).
scope :liked_by, ->(user) { joins(:likes).where(likes: { user_id: user.id }) }

Related

Looking for advise: Cleanest way to write scopes for filtering purposes [closed]

Closed. This question is opinion-based. It is not currently accepting answers.
Want to improve this question? Update the question so it can be answered with facts and citations by editing this post.
Closed 2 years ago.
Improve this question
Purpose of the post
I'm writing some code here to get an advise from people and see how they're writing clean ruby / rails code.
We're going to assume we have two models, User and Project. I wish to know how you'd create filters / scopes / methods for the best possible clean code.
class User < ApplicationRecord
# first_name, last_name, email
has_many :projects
end
class Project < ApplicationRecord
# title, description, published
belongs_to :user
end
Method 1
class User < ApplicationRecord
# first_name, last_name, email
has_many :projects
scope :with_first_name, -> (value) { where(first_name: value) }
scope :with_last_name, -> (value) { where(last_name: value) }
scope :with_email, -> (value) { where(email: value) }
scope :with_project_name, -> (value) { joins(:projects).where(projects: { name: value }) }
end
class UserController < ApplicationController
def index
#users = User.all
if params[:first_name].present?
#users = #users.with_first_name(params[:first_name])
end
if params[:last_name].present?
#users = #users.with_last_name(params[:last_name])
end
if params[:email].present?
#users = #users.with_email(params[:email])
end
if params[:project_name].present?
#users = #users.with_project_name(params[:project_name])
end
end
end
This can be useful, but we're gonna have a very fat controller. When we add more filters, we're going to have more and more conditions to fill.
It can also be refactored to:
class UserController < ApplicationController
def index
#users = User.all
{
first_name: :with_first_name,
last_name: :with_last_name,
email: :with_email,
project_name: :with_project_name,
}.each do |param, scope|
value = params[param]
if value.present?
#users = #users.public_send(scope, value)
end
end
end
end
but it will eliminate the possibility of having multiple params for a scope.
Method 2
Same as above, but in the model instead of controller:
class User < ApplicationRecord
# first_name, last_name, email
has_many :projects
scope :with_first_name, -> (value) { value ? where(first_name: value) : all }
scope :with_last_name, -> (value) { value.present ? where(last_name: value) : all }
scope :with_email, -> (value) { value.present ? where(email: value) : all }
scope :with_project_name, -> (value) { value.present? joins(:projects).where(projects: { name: value }) : all }
def self.filter(filters)
users = User.all
{
first_name: :with_first_name,
last_name: :with_last_name,
email: :with_email,
project_name: :with_project_name,
}.each do |param, scope|
value = filters[param]
if value.present?
users = users.public_send(scope, value)
end
end
users
end
end
class UserController < ApplicationController
def index
#users = User.filter(
params.permit(:first_name, :last_name, :email, :project_name)
)
end
end
Method 2
class User < ApplicationRecord
# first_name, last_name, email
has_many :projects
scope :with_first_name, -> (value) { value ? where(first_name: value) : all }
scope :with_last_name, -> (value) { value.present ? where(last_name: value) : all }
scope :with_email, -> (value) { value.present ? where(email: value) : all }
scope :with_project_name, -> (value) { value.present? joins(:projects).where(projects: { name: value }) : all }
end
class UserController < ApplicationController
def index
#users = User.all
#users = #users.with_first_name(params[:first_name])
#users = #users.with_last_name(params[:last_name])
#users = #users.with_email(params[:email])
#users = #users.with_project_name(params[:project_name])
end
end
This way, we add the value validation on the scope level, and we remove the param checking in the controller.
However, the repetition here is tremendous and would always return values even if the scope doesn't apply. ( ex: empty string ).
Final note
This post might not seem SO related, but would appreciate the input that anyone is going to give.
None of the above.
I would say that the cleanest way is to neither burdon your controller or User model further. Instead create a separate object which can be tested in isolation.
# Virtual model which represents a search query.
class UserQuery
include ActiveModel::Model
include ActiveModel::Attributes
attribute :first_name
attribute :last_name
attribute :email
attribute :project_name
# Loops through the attributes of the object and contructs a query
# will call 'filter_by_attribute_name' if present.
# #param [ActiveRecord::Relation] base_scope - is not mutated
# #return [ActiveRecord::Relation]
def resolve(base_scope = User.all)
valid_attributes.inject(base_scope) do |scope, key|
if self.respond_to?("filter_by_#{key}")
scope.merge(self.send("filter_by_#{key}"))
else
scope.where(key => self.send(key))
end
end
end
private
def filter_by_project_name
User.joins(:projects)
.where(projects: { name: project_name })
end
# Using compact_blank is admittedly a pretty naive solution for testing
# if attributes should be used in the query - but you get the idea.
def valid_attributes
attributes.compact_blank.keys
end
end
This is especially relevant when you're talking about a User class which usually is the grand-daddy of all god classes in a Rails application.
The key to the elegance here is using Enumerable#inject which lets you iterate accross the attributes and add more and more filters successively and ActiveRecord::SpawnMethods#merge which lets you mosh scopes together. You can think of this kind of like calling .where(first_name: first_name).where(last_name: last_name)... except in a loop.
Usage:
#users = UserQuery.new(
params.permit(:first_name, :last_name, :email, :project_name)
).resolve
Having a model means that you can use it for form bindings:
<%= form_with(model: #user_query, url: '/users/search') do |f| %>
# ...
<% end %>
And add validations and other features without making a mess.
scope :filter_users, -> (params) { where(conditions).with_project_name }
scope :with_project_name, -> (value) { value.present? joins(:projects).where(projects: { name: value }) : all }
def process_condition(attr, hash)
value = params[attr]
return hash if value.blank?
hash[attr] = value
hash
end
#This will return the conditions hash to be supplied to where. Since the param may have some other attribute which we may not need to apply filter, we construct conditions hash here.
def conditions
hash = {}
%i[last_name first_name email].each do |attr|
hash = process_condition(attr, hash)
end
hash
end
Finally, I would recommend you to check out ransack gem and the demo for the app is ransack demo. You can just use the search result of this gem by which you can support more filter options.

How to unscope multiple models in rails?

I am trying to unscope multiple model as below
User Model which has acts_as_paranoid
class User
acts_as_paranoid
has_one :category
has_one :brand
has_one :item
INDEXED_FIELDS = {
only: [:name],
include: {
category: { only: [:name] },
item: { only:[:name] },
brand: { only: [:name]},
}
}
def custom_json
Category.unscoped do
Item.unscoped do
Brand.unscoped do
self.as_json(INDEXED_FIELDS)
end
end
end
end
end
User model has following association which also has acts_as_paranoid
Sample Category model, Brand and Item model have same code
class Category
acts_as_paranoid
belongs_to :user
end
Can I do this dynamically with 'N' number of models, like iterating over array as below
def custom_json
[Category, Item, Brand].each do
# do unscoping
end
end
Association looks like
I think the approach you may have is to unscope the class manually, by setting default_scopes to [], and then putting it back.
classes_to_unscope = [Category, Item, Brand]
# remove default_scopes, saving them in previous_scopes
previous_scopes = classes_to_unscope.map do |klazz|
scopes = klazz.default_scopes
klazz.default_scopes = []
scopes
end
self.as_json(INDEXED_FIELDS)
# put default_scopes back
classes_to_unscope.each_with_index do |klazz, i|
klazz.default_scopes = previous_scopes[i]
end
As extra method:
def unscope_all(*models, &block)
# the order does not matter, but preserve it
blocks = [block] + models.reverse.map do |model|
proc do |inner_block|
model.unscoped { inner_block.call }
end
end
blocks.inject do |inner, outer|
proc { outer.call(inner) }
end.call
end
Then you would use it:
unscope_all(Category, Item, Brand) do
# do unscoping
end
unscoped pitfall: when leaving the block you loose the "unscopability", so make sure you don't return a relation (it won't be unscoped). Instead you have to resolve it in the block (e.g. by returning an array where(...).to_a.

ActiveAdmin issue with Scoped Resource

I have the following Scope on a resource page:
scope("Current Active Event Registrations") { |scope| Event.current_active_event_registrations }
The error I keep getting when viewing the page is:
undefined method `except' for nil:NilClass
c = c.except :select, :order
The code in Event looks like:
class Event < ActiveRecord::Base
has_many :registrations
scope :active_event, -> { where(active: true) }
scope :not_expired_active, -> { active_event.where('end_at > ?', DateTime.now) }
after_save :check_for_other_active_events
def random_winner
self.registrations.order("RANDOM()").first
end
def self.current_active_event_registrations
events = self.not_expired_active
events.first.registrations unless events.blank?
all if events.blank?
end
private
def check_for_other_active_events
Event.where('id != ?', self.id).update_all(active: false) if self.active
end
end
I am just wanting to add in a custom scope to my Registration Resource page in my ActiveAdmin backend.
I'm using Rails 4 and the latest ActiveAdmin
def self.current_active_event_registrations
events = self.not_expired_active
if events.blank?
all
else
events.first.registrations
end
end

Call a Method Model inside Controller

I have the following model;
(app/models/student_inactivation_log.rb)
class StudentInactivationLog < ActiveRecord::Base
belongs_to :student
belongs_to :institution_user
belongs_to :period
validates_presence_of :student_id, :inactivated_on, :inactivation_reason
INACTIVATION_REASONS = [{ id: 1, short_name: "HTY", name: "You didn't study enough!"},
{ id: 2, short_name: "KS", name: "Graduated!"},
{ id: 3, short_name: "SBK",name: "Other Reason"}]
Class methods
class << self
def inactivation_reason_ids
INACTIVATION_REASONS.collect{|v| v[:id]}
end
def inactivation_reason_names
INACTIVATION_REASONS.collect{|v| v[:name]}
end
def inactivation_reason_name(id)
INACTIVATION_REASONS.select{|t| t[:id] == id}.first[:name]
end
def inactivation_reason_short_name(id)
INACTIVATION_REASONS.select{|t| t[:id] == id}.first[:short_name]
end
def inactivation_reason_id(name)
INACTIVATION_REASONS.select{|t| t[:name] == name}.first[:id]
end
end
# Instance methods
def inactivation_reason_name
self.class.inactivation_reason_name(self.inactivation_reason)
end
def inactivation_reason_short_name
self.class.inactivation_reason_short_name(self.inactivation_reason)
end
def inactivation_reason_id
self.class.inactivation_reason_id(self.inactivation_reason)
end
end
I would like to call these inactivation reasons from my controller, which is app/controllers/student/session_controllers.rb file:
class Student::SessionsController < ApplicationController
layout 'session'
def create
student = Student.authenticate(params[:student_number], params[:password])
if student.active
session[:student_id] = student.id
redirect_to student_main_path, :notice => 'Welcome!'
elsif (student and student.student_status == 3) or (student and !student.active)
flash.now.alert = "You can't login because #REASON_I_AM_TRYING_TO_CALL"
render 'new'
else
....
end
end
I would like to show students their inactivation reason on the systems if they can't login.
How can I call my INACTIVATION_REASONS from this controller file? Is it possible?
Thanks in advance!
That's just a constant, so you can call it as constant anywhere.
StudentInactivationLog::INACTIVATION_REASONS
Update
I realized actually what you want is to use a reason code or short name saved in db to represent the string.
If so, I recommend you to use the short name directly as Hash. "id" looks redundant for this light case.
INACTIVATION_REASONS = {"HTY"=>"You didn't study enough!",
"KS"=>"Graduated!",
"SBK"=>"Other Reason"}
validates :inactivation_reason, inclusion: { in: INACTIVATION_REASONS.keys,
message: "%{value} is not a valid short name" }
def full_reason_message
INACTIVATION_REASONS[self.inactivation_reason]
end
Then, to show full message of a reason in controller
reason = #student.full_reason_message
This is the idea. I havn't checked your other model codes. You'll need to save reason as the short name instead of id, and need to revise/remove some code if you decide to use it in this way.

How to return an unchanged AR relation from a class method

I have code like this:
class Item < ActiveRecord::Base
class << self
def for_category(c)
if c
return where(:category_id => c.id)
else
return self
end
end
end
end
I need to call it like this:
Item.where("created_at > ?", Time.now - 1.week).for_category(#category)
#category may or may not be null. In the case where category is null, I want the method to simply pass through and return the relation unchanged. Of course, return self simply returns the Item class.
What would be the correct way to do this?
Are you trying to return the Scope (as opposed to the Class itself) for further scope action? If so, then something like the following should work:
class Item < ActiveRecord::Base
class << self
def for_category(c)
if c
return where(:category_id => c.id)
else
return scoped
end
end
end
end
HTH
class Item < ActiveRecord::Base
def self.for_category(c)
conditions = {:category_id => c.id}
conditions.delete_if {|key,val| val.blank? }
self.where(conditions)
end
end
Your Item is associated with Category ? If yes then you can simply get all item categories by Item.where("created_at > ?", Time.now - 1.week).categroies not need for above code.
#scoped was deprecated in Rails 4. You can use #all to achieve the same effect:
class Item < ActiveRecord::Base
class << self
def for_category(c)
if c
return where(:category_id => c.id)
else
return all
end
end
end
end

Resources