How to chain class methods together in Ruby on Rails? - ruby-on-rails

I've got this in my Rails 5 model:
def self.payable
open.where.not(:delivery_status => "draft")
end
def self.draft
where(:delivery_status => "draft")
end
def self.open
where(:payment_status => "open")
end
Is there a more elegant way to write the first method?
It would be great to chain the open and draft methods together like this:
def self.payable
open.not(:draft)
end
Unfortunately, this doesn't work.

To chain negated queries you can use this trick:
def self.payable
open.where.not(id: draft)
end
Another alternative if you don't care if an ActiveRecord::Relation object is returned is using -, which returns an Array:
def self.payable
open - draft
end
I would personally use scopes instead of class methods for queries: https://guides.rubyonrails.org/active_record_querying.html#scopes. So:
scope :draft, -> { where(:delivery_status => "draft") }
scope :open, -> { where(:payment_status => "open") }
scope :payable, -> { open.where.not(id: draft) }

Maybe you can use scopes?
scope :payable, -> { open.where.not(:delivery_status => "draft") }
You can use this like that
YouModel.payable

Related

How to use attr_encrypted with as_json and join and get the decrypted attribute?

I have an attribute encypted using attr_encrypted and I'm using as_json. Under some circumstances I don't want the ssn to be part of a API response, and other times I want it to be included but using the name ssn not encrypted_ssn and to show the decrypted value. In all my cases encrypted_ssn should not be included in the result of as_json.
My first question is, how do I get as_json to return the decrypted ssn field?
With this code
class Person
attr_encrypted :ssn, key: 'key whatever'
end
I want this
Person.first.as_json
=> {"id"=>1,
"ssn"=>"333-22-4444"}
What I don't want is this:
Person.include_ssn.first.as_json
=> {"id"=>1,
"encrypted_ssn"=>"mS+mwRIsMI5Y6AzAcNoOwQ==\n"}
My second question is, how do I make it so a controller using a model can choose to include the decrypted ssn in the JSON ("ssn"=>"333-22-4444") or exclude the field (no "encrypted_ssn"=>"mS+mwRIsMI5Y6AzAcNoOwQ==\n")? I don't even want encrypted values going out to the client if the controller doesn't explicitly specify to include it.
This is what I have so far and seems to work:
class Person
attr_encrypted :ssn, key: 'key whatever'
scope :without_ssn, -> { select( column_names - [ 'encrypted_ssn' ]) }
default_scope { without_ssn }
end
Person.first.as_json
=> {"id"=>1}
I haven't figured out how to make this work in a way that includes the decrypted ssn field as in the first question. What I would like is something like this:
Person.include_ssn.first.as_json
=> {"id"=>1,
"ssn"=>"333-22-4444"}
My final question is, how do I make the above work through a join and how do I specify to include or exclude the encrypted value (or scope) in the join?
With this code:
class Person
has_many :companies
attr_encrypted :ssn, key: 'key whatever'
scope :without_ssn, -> { select( column_names - [ 'encrypted_ssn' ]) }
default_scope { without_ssn }
end
class Company
belongs_to :person
end
This seems to work like I want it
Company.where(... stuff ...).joins(:person).as_json(include: [ :person ])
=> {"id"=>1,
"person"=>
{"id"=>1}}
But I don't know how to implement include_ssn like below or alternatives to tell the person model to include the ssn decrypted.
Company.where(... stuff ...).joins(:person).include_ssn.as_json(include: [ :person ])
=> {"id"=>1,
"person"=>
{"id"=>1,
"ssn"=>"333-22-4444"}}
I've solved this in a different way. Originally I was doing this:
app/models/company.rb
class Company
# ...
def self.special_get_people
people = Company.where( ... ).joins(:person)
# I was doing this in the Company model
people.instance_eval do
def as_json_with_ssn
self.map do |d|
d.as_json(except: [:encrypted_ssn] ).merge('ssn' => d.person.ssn)
end
end
def as_json(*params)
if params.empty?
super(except: [:encrypted_ssn] ).map{ |p| p.merge('ssn' => nil) }
else
super(*params)
end
end
end
return people
end
end
app/controllers/person_controller.rb
class PersonController < ApplicationController
def index
#people = Company.special_get_people
# Then manually responding with JSON
respond_to do |format|
format.html { render nothing: true, status: :not_implemented }
format.json do
render json: #people.as_json and return unless can_view_ssn
render json: #people.as_json_with_ssn
end
end
end
end
However this was fragile and error prone. I've since refactored the above code to look more like this:
app/models/company.rb
class Company
# ...
def self.special_get_people
Company.where( ... ).joins(:person)
end
end
app/controllers/person_controller.rb
class PersonController < ApplicationController
def index
#people = Company.special_get_people
end
end
app/views/person/index.jbuilder
json.people do
json.array!(#people) do |person|
json.extract! person, :id # ...
json.ssn person.ssn if can_view_ssn
end
end
And this ends up being a much better solution that's more flexible, more robust and easier to understand.

Ruby on Rails search with multiple parameters

For example in my Car model i have such fields:
color, price, year
and in form partial i generate form with all this fields. But how to code such logic:
user could enter color and year and i must find with this conditions, user could enter just year or all fields in same time...
And how to write where condition? I could write something like:
if params[:color].present?
car = Car.where(color: params[:color])
end
if params[:color].present? && params[:year].present?
car = Car.where(color: params[:color], year: params[:year])
end
and so over....
But this is very ugly solution, i'm new to rails, and want to know: how is better to solve my problem?
Check out the has_scope gem: https://github.com/plataformatec/has_scope
It really simplifies a lot of this:
class Graduation < ActiveRecord::Base
scope :featured, -> { where(:featured => true) }
scope :by_degree, -> degree { where(:degree => degree) }
scope :by_period, -> started_at, ended_at { where("started_at = ? AND ended_at = ?", started_at, ended_at) }
end
class GraduationsController < ApplicationController
has_scope :featured, :type => :boolean
has_scope :by_degree
has_scope :by_period, :using => [:started_at, :ended_at], :type => :hash
def index
#graduations = apply_scopes(Graduation).all
end
end
Thats it from the controller side
I would turn those into scopes on your Car model:
scope :by_color, lambda { |color| where(:color => color)}
scope :by_year, lambda { |year| where(:year => year)}
and in your controller you would just conditionally chain them like this:
def index
#cars = Car.all
#cars = #cars.by_color(params[:color]) if params[:color].present?
#cars = #cars.by_year(params[:year]) if params[:year].present?
end
user_params = [:color, :year, :price]
cars = self
user_params.each do |p|
cars = cars.where(p: params[p]) if params[p].present?
end
The typical (naive, but simple) way I would do this is with a generic search method in my model, eg.
class Car < ActiveRecord::Base
# Just pass params directly in
def self.search(params)
# By default we return all cars
cars = all
if params[:color].present?
cars = cars.where(color: params[:color])
end
if params[:price1].present? && params[:price2].present?
cars = cars.where('price between ? and ?', params[:price1], params[:price2])
end
# insert more fields here
cars
end
end
You can easily keep chaining wheres onto the query like this, and Rails will just AND them all together in the SQL. Then you can just call it with Car.search(params).
I think you could use params.permit
my_where_params = params.permit(:color, :price, :year).select {|k,v| v.present?}
car = Car.where(my_where_params)
EDIT: I think this only works in rails 4, not sure what version you're using.
EDIT #2 excerpt from site I linked to:
Using permit won't mind if the permitted attribute is missing
params = ActionController::Parameters.new(username: "john", password: "secret")
params.permit(:username, :password, :foobar)
# => { "username"=>"john", "password"=>"secret"}
as you can see, foobar isn't inside the new hash.
EDIT #3 added select block to where_params as it was pointed out in the comments that empty form fields would trigger an empty element to be created in the params hash.

First or limit in Rails scope

I have the following scope:
scope :user_reviews, lambda { |user| where(:user_id => user) }
I apply this in the controller:
def show
#review = #reviewable.reviews.user_reviews(current_user).first || Review.new
end
The first is to limit to search the current user's one and only review. Now I try to write a new scope user_review which I tried many ways to chain the user_reviews scope with first, but just couldn't get it what. Something like this:
scope :user_reviews, lambda { |user| where(:user_id => user) }
scope :user_review, lambda { |user| user_reviews(user).first }
I know the above user_review is wrong, but just trying to show you guys what I am trying to do.
How should I write this properly?
Thanks.
#Victor, just stick with your original idea. Use scope :user_reviews, lambda { |user| where(:user_id => user) } and call user_reviews.first. Nothing wrong with that.
Definitely do not define a scope that returns a single object. A scope should be chainable.
I also used this:
def self.user_review(user)
self.user_reviews(user).first
end
I feel this use case is very common.
We can solve this by following:
scope :user_reviews, lambda { | user_id | where(:user_id => user_id) }
and then in the model have user_review as class method
def self.user_review(user_id)
user_reviews(user_id).first
end
Now you can call user_review and it'll return the one record.
scope :user_reviews, ->(user) { where(user_id: :user) }

Filtering results in Rails

I am showing a list of questions when the user use the index action. I want to filter this list, showing only rejected questions, questions that only have images attached to them etc.
How do you do that? Do you just add code in the index action that checks if the different named parameters is in the request parameter hash and use them build a query.
myurl.com/questions?status=approved&only_images=yes
Or are there better ways?
You can use has_scope to do this elegantly:
# Model
scope :status, proc {|status| where :status => status}
scope :only_images, ... # query to only include questions with images
# Controller
has_scope :status
has_scope :only_images, :type => boolean
def index
#questions = apply_scopes(Question).all
end
To keep your controller thin and avoid spaghetti code you can try to use following way:
Controller:
def index
#questions = Question.filter(params.slice(:status, :only_images, ...) # you still can chain with .order, .paginate, etc
end
Model:
def self.filter(options)
options.delete_if { |k, v| v.blank? }
return self.scoped if options.nil?
options.inject(self) do |scope, (key, value)|
return scope if value.blank?
case key
when "status" # direct map
scope.scoped(:conditions => {key => value})
when "only_images"
scope.scoped(:conditions => {key => value=="yes" ? true : false})
#just some examples
when "some_field_max"
scope.scoped(:conditions => ["some_field <= ?", value])
when "some_field_min"
scope.scoped(:conditions => ["some_field >= ?", value])
else # unknown key (do nothing. You might also raise an error)
scope
end
end
end
So, I think there are places where you need to code to be good in such a scenario; the model and the controller.
For the model you should use scopes.
#Model
scope :rejected, lambda { where("accepted = false") }
scope :accepted lambda { where("accepted = true") }
scope :with_image # your query here
In the controller,
def index
#questions = #questions.send(params[:search])
end
You can send the method name from the UI and directly pass that to the scope in the model. Also, you can avoid an "if" condition for the .all case by passing it from the UI again.
But as this directly exposes Model code to view, you should filter any unwanted filter params that come from the view in a private method in the controller using a before_filter.

How to access named_scope arguments from named scope extension?

following example:
named_scope :search, lambda {|my_args| {...}} do
def access_my_args
p "#{my_args}"
end
end
# Call:
Model.search(args).access_my_args
As you can see I want to access the arguments from the lambda in the named_scope extension. Is there a way to do this?
A more specific example:
class User < ActiveRecord::Base
named_scope :by_name, lambda {|name_from_scope| {:conditions => {:name => name_from_scope}}} do
def change_name
each { |i| i.update_attribute(:name, "#{name_from_scope}xyz") }
end
end
end
(I know that there is a find_by_name scope and so on...). I want to use the name_from_scope argument, that is passed in the scope in the scope extension.
named_scope :test_scope, lambda {|id| {:conditions => {:id => id}} } do
def test_scope_method
each {|i| puts #proxy_options.to_yaml}
end
end
I don't believe you can get to the arguments directly without extending activerecord.
#proxy_options will give you the compiled options in the block. So, in your example, you won't have access to name_from_scope but you will have access to #proxy_options[:conditions][:name].
Is this what you're trying to do?
named_scope :search, lambda {|*my_args|
OtherClass.announce_search_for_model(my_args, self.class)
{ :conditions => ['created_at < ?', my_args[:created_at]], :limit => my_args[:limit] }
}
args = {:created_at > 'NOW()', :limit => 5}
Model.search(args)
If you're wanting to observe what's passed onto the named_scope then I would do that in the lambda.
Named_scope results will always be a result as if you'd used Model.find. This is a functionality of rails so you need to override rails functionality with a Module if you want something different. I wouldn't recommend doing that because named_scope extensions are there for simplifying finders, not observing parameters.

Resources