Using ActiveRecord's scope instruction to return a single result - ruby

I have the following class that works fine in my unit tests but I feel it could be simpler.
class License < ActiveRecord::Base
scope :active, lambda {
where("active_from <= ?", Date.today).order("version_number DESC")
}
end
def current_license
return License.active.first
end
public :current_license
I have tried appending the .first into the lambda clause but that causes an error.
How do I tell the scope I only want the first result, and thus eliminate the method current_license completely?

Make it a method, and you can get this:
class License < ActiveRecord::Base
def self.current_license
return License.where("active_from <= ?", Date.today).order("version_number DESC").first
end
end
As for the number of results, try to add a .limit(1). For more info, have a look here.

Related

Metrics/AbcSize Too High: How do I decrease the ABC in this method?

I have recently started using Rubocop to "standardise" my code, and it has helped me optimise a lot of my code, as well as help me learn a lot of Ruby "tricks". I understand that I should use my own judgement and disable Cops where necessary, but I have found myself quite stuck with the below code:
def index
if params[:filters].present?
if params[:filters][:deleted].blank? || params[:filters][:deleted] == "false"
# if owned is true, then we don't need to filter by admin
params[:filters][:admin] = nil if params[:filters][:admin].present? && params[:filters][:owned] == "true"
# if admin is true, then must not filter by owned if false
params[:filters][:owned] = nil if params[:filters][:owned].present? && params[:filters][:admin] == "false"
companies_list =
case params[:filters][:admin]&.to_b
when true
current_user.admin_companies
when false
current_user.non_admin_companies
end
if params[:filters][:owned].present?
companies_list ||= current_user.companies
if params[:filters][:owned].to_b
companies_list = companies_list.where(owner: current_user)
else
companies_list = companies_list.where.not(owner: current_user)
end
end
else
# Filters for deleted companies
companies_list = {}
end
end
companies_list ||= current_user.companies
response = { data: companies_list.alphabetical.as_json(current_user: current_user) }
json_response(response)
end
Among others, the error that I'm getting is the following:
C: Metrics/AbcSize: Assignment Branch Condition size for index is too high. [<13, 57, 16> 60.61/15]
I understand the maths behind it, but I don't know how to simplify this code to achieve the same result.
Could someone please give me some guidance on this?
Thanks in advance.
Well first and foremost, is this code fully tested, including all the myriad conditions? It's so complex that refactoring will surely be disastrous unless the test suite is rigorous. So, write a comprehensive test suite if you don't already have one. If there's already a test suite, make sure it tests all the conditions.
Second, apply the "fat model skinny controller" paradigm. So move all the complexity into a model, let's call it CompanyFilter
def index
companies_list = CompanyFilter.new(current_user, params).list
response = { data: companies_list.alphabetical.as_json(current_user: current_user) }
json_response(response)
end
and move all those if/then/else statements into the CompanyFilter#list method
tests still pass? great, you'll still get the Rubocop warnings, but related to the CompanyFilter class.
Now you need to untangle all the conditions. It's a bit hard for me to understand what's going on, but it looks as if it should be reducible to a single case statement, with 5 possible outcomes. So the CompanyFilter class might look something like this:
class CompanyFilter
attr_accessors :current_user, :params
def initialize(current_user, params)
#current_user = current_user
#params = params
end
def list
case
when no_filter_specified
{}
when user_is_admin
#current_user.admin_companies
when user_is_owned
# etc
when # other condition
# etc
end
end
private
def no_filter_specified
#params[:filter].blank?
end
def user_is_admin
# returns boolean based on params hash
end
def user_is_owned
# returns boolean based on params hash
end
end
tests still passing? perfect! [Edit] Now you can move most of your controller tests into a model test for the CompanyFilter class.
Finally I would define all the different companies_list queries as scopes on the Company model, e.g.
class Company < ApplicationRecord
# some examples, I don't know what's appropriate in this app
scope :for_user, ->(user){ where("...") }
scope :administered_by, ->(user){ where("...") }
end
When composing database scopes ActiveRecord::SpawnMethods#merge is your friend.
Post.where(title: 'How to use .merge')
.merge(Post.where(published: true))
While it doesn't look like much it lets you programatically compose scopes without overelying on mutating assignment and if/else trees. You can for example compose an array of conditions and merge them together into a single ActiveRecord::Relation object with Array#reduce:
[Post.where(title: 'foo'), Post.where(author: 'bar')].reduce(&:merge)
# => SELECT "posts".* FROM "posts" WHERE "posts"."title" = $1 AND "posts"."author" = $2 LIMIT $3
So lets combine that with a skinny controllers approach where you handle filtering in a seperate object:
class ApplicationFilter
include ActiveModel::Attributes
include ActiveModel::AttributeAssignment
attr_accessor :user
def initialize(**attributes)
super()
assign_attributes(attributes)
end
# A convenience method to both instanciate and apply the filters
def self.call(user, params, scope: model_class.all)
return scope unless params[:filters].present?
scope.merge(
new(
permit_params(params).merge(user: user)
).to_scope
)
end
def to_scope
filters.map { |filter| apply_filter(filter) }
.compact
.select {|f| f.respond_to?(:merge) }
.reduce(&:merge)
end
private
# calls a filter_by_foo method if present or
# defaults to where(key => value)
def apply_filter(attribute)
if respond_to? "filter_by_#{attribute}"
send("filter_by_#{attribute}")
else
self.class.model_class.where(
attribute => send(attribute)
)
end
end
# Convention over Configuration is sexy.
def self.model_class
name.chomp("Filter").constantize
end
# filters the incoming params hash based on the attributes of this filter class
def self.permit_params
params.permit(filters).reject{ |k,v| v.blank? }
end
# provided for modularity
def self.filters
attribute_names
end
end
This uses some of the goodness provided by Rails to setup objects with attributes that will dynamically handle filtering attributes. It looks at the list of attributes you have declared and then slices those off the params and applies a method for that filter if present.
We can then write a concrete implementation:
class CompanyFilter < ApplicationFilter
attribute :admin, :boolean, default: false
attribute :owned, :boolean
private
def filter_by_admin
if admin
user.admin_companies
else
user.non_admin_companies
end
end
# this should be refactored to use an assocation on User
def filter_by_owned
case owned
when nil
nil
when true
Company.where(owner: user)
when false
Company.where.not(owner: user)
end
end
end
And you can call it with:
# scope is optional
#companies = CompanyFilter.call(current_user, params), scope: current_user.companies)

Refactor with Strategy Pattern. In Ruby

Heads up! In the below example, using a pattern is probably overkill... however, if I were extending this to count genres, count the members in a given band, count the number of fans, count the number of venues played, count the number of records sold, count the number of downloads for a specific song etc... it seems like there could be a ton of stuff to count.
The Goal:
To create a new function that chooses the correct counting function based on the input.
The Example:
class Genre < ActiveRecord::Base
has_many :songs
has_many :artists, through: :songs
def song_count
self.songs.length
end
def artist_count
self.artists.length
end
end
P.S. If you are also a curious about this question, you may find this other question (unfortunately answered in C#) to be helpful as a supplemental context. Strategy or Command pattern? ...
In Ruby you can implement a strategy pattern quite easily using an (optional) block (assuming it's still unused).
class Genre < ActiveRecord::Base
has_many :songs
has_many :artists, through: :songs
def song_count(&strategy)
count_using_strategy(songs, &strategy)
end
def artist_count(&strategy)
count_using_strategy(artists, &strategy)
end
private
def count_using_strategy(collection, &strategy)
strategy ||= ->(collection) { collection.size }
strategy.call(collection)
end
end
The above code defaults to using the size strategy. If you ever want to use a specific strategy in a specific scenario you can simply provide the strategy alongside the call.
genre = Genre.last
genre.song_count # get the song_count using the default #size strategy
# or provide a custom stratigy
genre.song_count { |songs| songs.count } # get the song_count using #count
genre.song_count { |songs| songs.length } # get the song_count using #length
If you need to re-use some strategies more often you could save them in a constant or variable:
LENGTH_STRATEGY = ->(collection) { collection.length }
genre.artist_count(&LENGTH_STRATEGY)
Or create a specific class for them if they are more complicated (currently overkill):
class CollectionStrategy
def self.to_proc # called when providing the class as a block argument
->(collection) { new(collection).call }
end
attr_reader :collection
def initialize(collection)
#collection = collection
end
end
class LengthStrategy < CollectionStrategy
def call
collection.length
end
end
genre.artist_count(&LengthStrategy)

Sweeper never triggers

I have the following usecase: I have an API and a UI which both modify the same type of object Vehicle. However, when a vehicle is modified from the UI, it must have a Vehicle Identification Number provided, while if it is modified, that number may not yet be known as it has not yet been stamped on the Engine Block (this is clearly a manufactured example).
So my model is:
class Vehicle < ActiveRecord::Base
after_initialize :set_ivars
def set_ivars
#strict_vid_validation = true
end
validate :vid, length: {maximum: 100, minimum: 30}, presence: true, if: lambda { |o| o.instance_variable_get(:#strict_vid_validation) }
validate :custom_vid_validator, unless: lambda { |o| o.instance_variable_get(:#strict_vid_validation) }
end
Two parent controllers:
class ApplicationController < ActionController::Base
before_filter :set_api_filter
private
def set_api_filter
#api = false
end
end
and an ApiController that sets #api to true
and the following vehicle controller:
class VehicleController < ApplicationController
cache_sweeper VehicleSweeper, only: [:create, :update]
def create
#vehicle = Vehicle.new
#vehicle.update_attributes(params[:vehicle])
end
end
with the following sweeper:
class VehicleSweeper < ActionController::Caching::Sweeper
observe Vehicle
def before_validation(vehicle)
if self.instance_variable_get(:#api)
vehicle.instance_variable_set(:#strict_vid_validation, false)
else
vehicle.instance_variable_set(:#strict_vid_validation, true)
end
end
And a somewhat similiar ApiVehicleController
However, this does not work. Through debugging I have discovered that:
1) The before_validation (nor do any other callbacks configured) method in the sweeper never runs
2) The more stringent ui validation is always triggered, this is due to
3) inside the lambda on the if: on the validate, the instance_variable is always true as it is never set through a callback method
Why is this not working? How can I fix it? Is there a different approach I could take if not?
In the end, the following worked:
def my_controller_action
#vehicle = Vehicle.new
#vehicle.assign_attributes(params[:vehicle])
VehicleSweeper.send(:new).pick_a_validation(#vehicle)
#vehicle.save
end
and in the sweeper removing the line
observe Vehicle
and renaming the before_validation method to pick_a_validation.
Of course this only works because I wanted to do a before_validation. If I wanted to do say an after_validation I have no idea how I would have hacked it.
In either case, I would love to hear how this is done without bypassing the callback chain entirely like I did.

ActiveRecord Scope: Strange behaviour with select on relation

I have two different models with a 1:N relation.
Let's name them 'myobject' and 'related'
class Myobject < ActiveRecord::Base
has_many :related
scope :without_related, includes(:related).select{ |o| o.related.size == 0 }
end
class Related < ActiveRecord::Base
end
The defined scope seems to work great as long as I don't create new assignments from Myobjects to Related:
Direct rails c command "Myobject.includes(:related).select ... (as defined in Scope) works as expected
Calls to scope "Myobject.without_related" still return objects that have been assigned in the meantime
It seems that this can be fixed by restarting the rails console or restarting Webrick.
But I can't always restart a webapplication only because a relation between objects has been changed ;)
Is there any way to fix this problem or to write the scope in a better way?
PS: I need this query as scope to pass its name as group_method to a grouped_select in the form of the Myobject model
Your problem is that in fact your scope is not scope :)
Scopes must return relations, but your scope returns array.
Though it can work as you expect, if you wrap it in lambda
scope :without_related, lambda{ includes(:related).select{ |o| o.related.size == 0 } }
But I recommend to rewrite this code as usual class method to not mislead those who'll work with this code in future
def self.without_related
includes(:related).select{ |o| o.related.size == 0 }
end
or use counter cache, as advised in other answer.
I would recommend you to use counter_cache for this, you need to add column *related_count* of type int to Myobject, make migration and then you will be able to do so:
class Myobject < ActiveRecord::Base
has_many :related
scope :without_related, where(related_count: 0)
end
class Related < ActiveRecord::Base
belongs_to :myobject, counter_cache: true
end
After that you will have super fast scope for getting all objects with no related records and an a count of that objects as well
Or if you know column name that should be present in related table, use this definition:
class Myobject < ActiveRecord::Base
has_many :related
scope :without_related, includes(:related).where('related.id', true)
end

Static local variables for methods in Ruby?

I have this:
def valid_attributes
{ :email => "some_#{rand(9999)}#thing.com" }
end
For Rspec testing right? But I would like to do something like this:
def valid_attributes
static user_id = 0
user_id += 1
{ :email => "some_#{user_id}#thing.com" }
end
I don't want user_id to be accessible from anywhere but that method,
is this possible with Ruby?
This is a closure case. Try this
lambda {
user_id = 0
self.class.send(:define_method, :valid_attributes) do
user_id += 1
{ :email => "some_#{user_id}#thing.com" }
end
}.call
Wrapping everything in lambda allows the variables defined within lambda to only exist in the scope. You can add other methods also. Good luck!
This answer is a little larger in scope than your question, but I think it gets at the root of what you're trying to do, and will be the easiest and most maintainable.
I think what you're really looking for here is factories. Try using something like factory_girl, which will make a lot of testing much easier.
First, you'd set up a factory to create whatever type of object it is you're testing, and use a sequence for the email attribute:
FactoryGirl.define do
factory :model do
sequence(:email) {|n| "person#{n}#example.com" }
# include whatever else is required to make your model valid
end
end
Then, when you need valid attributes, you can use
Factory.attributes_for(:model)
You can also use Factory.create and Factory.build to create saved and unsaved instances of the model.
There's explanation of a lot more of the features in the getting started document, as well as instructions on how to add factories to your project.
You can use a closure:
def validator_factory
user_id = 0
lambda do
user_id += 1
{ :email => "some_#{user_id}#thing.com" }
end
end
valid_attributes = validator_factory
valid_attributes.call #=> {:email=>"some_1#thing.com"}
valid_attributes.call #=> {:email=>"some_2#thing.com"}
This way user_id won't be accessible outside.
I'd use an instance variable:
def valid_attributes
#user_id ||= 0
#user_id += 1
{ :email => "some_#{#user_id}#thing.com" }
end
The only variables Ruby has are local variables, instance variables, class variables and global variables. None of them fit what you're after.
What you probably need is a singleton that stores the user_id, and gives you a new ID number each time. Otherwise, your code won't be thread-safe.

Resources