Add scope in concerns for various models - ruby-on-rails

In apps/models/concerns/deactivable.rb
module Deactivatable
extend ActiveSupport::Concern
included do
scope :alive, -> { where(:deactivated_at => nil) }
end
def deactivate(t = Time.current)
update_attribute(:deactivated_at,t)
end
def activate
update_attribute(:deactivated_at,nil)
end
def deactivated?
deactivated_at.present?
end
end
This is being included in 2 models, app/models/activity_rules/activity_detection_rule.rb and app/models/concerns/generic_campaign.rb.
There are 2 more models which contain the same methods with different attribute name.
In redeemable.rb,
class Redeemable < ActiveRecord::Base
scope :alive, -> { where("(deactivation_date is null) and (expiry_date is null or expiry_date >= ?)",Date.today) }
def deactivate(t = Time.current)
update_attribute(:deactivation_date,t)
end
def reactivate
update_attribute(:deactivation_date,nil)
end
def deactivated?
deactivation_date.present?
end
end
and in surprise_set.rb
scope :alive, -> { where("deactivation_date is null") }
with the same 3 methods as redeemable.rb.
How to use Deactivable concern to DRY up the other two models?

You could return the attribute that indicates the time of deactivation from a class method. You can provide a default implementation in your concern and override in the class that includes the concern if you need to:
module Deactivatable
extend ActiveSupport::Concern
included do
scope :alive, -> { where(deactive_attr => nil) }
def self.deactive_attr
:deactivated_at
end
end
def deactivate(t = Time.current)
update_attribute(self.class.deactive_attr, t)
end
def activate
update_attribute(self.class.deactive_attr, nil)
end
def deactivated?
self.send(self.class.deactive_attr).present?
end
end
Then, in classes where you want to provide a different attribute you can add a class method:
include Deactivatable
def self.deactive_attr
:deactivation_date
end
You could also DRY up your alive scope a bit by allowing the class that includes the concern to define the conditions for 'aliveness'. In the concern you can define the default
scope :alive, -> { where(self.active_conditions) }
def self.active_conditions
{ self.deactive_attr => nil }
end
You can then provide a different implementation of active_conditions in the class itself:
self self.active_conditions
["(deactivation_date is null) and
(expiry_date is null or expiry_date >= ?)", Date.today]
end

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.

Rails class method not defined

I have a controller which calls a class method from a model. However, I got undefined method 'where' for Jira:Class.
controller:
module Api
module V1
class JiraController < ApplicationController
def index
jira = Jira.where()
jira_stat = JiraStat.new(jira)
render json: [
{
t('jira.api.status') => jira_stat.status,
t('jira.api.number_of_jiras') => jira_stat.jira_total
}
]
end
end
end
end
model:
# frozen_string_literal: true
require 'active_model'
class Jira
include ActiveModel::Model
include JiraKit
attr_accessor :status, :jira
def self.where(status_name = 'all')
if status_name == 'all'
jiras = JiraKit.where.jira_issues(status: ['open', 'submitted', 'in
progress', 'in review', 'closed'])
elsif
jiras = JiraKit.where.jira_issues(status: [status_name])
end
new(#status = status_name, #jira = jiras)
end
end
I think I have used self keyword. But I don't know why I can't access that method. If I create an instance of Jira model, I am able to access that method.

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

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