instance method in scope - ruby-on-rails-3

I don't know even if its possible? I need to use instance method with/within scope. Something like this:
scope :public, lambda{ where ({:public => true}) }
and call instance method(complete?) on each record to see if it is completed. public scope here should return all records that are public and are completed and completion of a record is determined by a instance method 'complete?'
Any possibility?
Thanks

Scopes are about generating query logic using ARel. If you can't represent the logic of the complete? method in SQL then you're kind of stuck
Scopes - in rails 3 at least - are meant for chaining together query logic without returning a result set. If you need a result set to do your testing for complete you'll need to do something like
class MyModel < ActiveRecord::Base
scope :public, lambda{ where ({:public => true}) }
def self.completed_public_records
MyModel.public.all.select { |r| r.completed? }
end
end
# elsewhere
MyModel.completed_public_records
Or if you need more flexibility
class MyModel < ActiveRecord::Base
scope :public, lambda{ where ({:public => true}) }
# some other scopes etc
def self.completed_filter(finder_obj)
unless finder_obj.is_a?(ActiveRecord::Relation)
raise ArgumentError, "An ActiveRecord::Relation object is required"
end
finder_obj.all.select { |r| r.completed? }
end
end
# elsewhere
MyModel.completed_filter(MyModel.public.another_scope.some_other_scope)

I created a rubygem for this exact problem a few months back when I had the same problem.
It allows you to add methods that work on the result set of the query, but abstracts the methods into another class so its not muddled in with your model.
Check it out: https://github.com/coryodaniel/collectively

Related

ActiveModelSerializer and VOID types responses

Solved!
Kids, don't define read_attribute_for_serialization to return void
So I'm trying to integrate sorbet in one of my rails apps. I've added sorbet in the Gemfile as so
gem "sorbet-runtime", "~> 0.5.5657"
gem "sorbet-rails", "~> 0.6.5.1"
gem "sorbet", "~> 0.5.5657", group: [:development, :test]
All looks good an I was able to add sigs to Price class and resolve issues with type checking inside the app.
The problem I'm experiencing is the return values from serializers
So the serializer looks roughly like this
# typed: false
# frozen_string_literal: true
class MySuperAwesomeSerializer < ActiveModel::Serializer
attributes :start_date
has_one :price, serializer: PriceSerializer
end
In specs, when I want to validate the return value from the associated (has_one) attribute I get this
{
start_date: 2020-05-22,
price: {
total_amount: T::Private::Types::Void::VOID,
taxless_amount: T::Private::Types::Void::VOID,
vat_amount: T::Private::Types::Void::VOID
}
}
Price serializer looks roughly like this
# typed: false
# frozen_string_literal: true
class PriceSerializer < ActiveModel::Serializer
attributes :taxless_amount, :total_amount, :vat_amount
end
I fail to understand what and why is exactly happening here. Thanks in advance
EDIT: added the "solution" at the top of the post
I haven’t encountered this problem before, but it looks like a potential sorbet bug.
T::Private::Types::Void::Void is the return type that sorbet swap out for methods that have void as the return type. In this case, you don’t define any sig for the attributes so it shouldn’t have stub out the returning value.
To work around, you can define explicit sig for those attributes like
sig { returns(Integer) }
def taxless_amount
...
end
I haven’t tried but this may also work
sig { returns(Integer) }
attributes :taxless_amount
Edit: can you show the actual PriceSerializer class btw? It looks like it works without problem for the attributes in the other Serializer.
If you define read_attribute_for_serialization don't do it this way:
sig { params(attr: Symbol).void }
def read_attribute_for_serialization(attr)
public_send(attr)
end

Validating Child Object with ActiveModel Validations

I have two plain Ruby classes, Account and Contact. I am using Simple Form's simple_form_for and simple_fields_for to create nested attributes. I am looking to fulfill the following validation requirements:
An associated Contact must exist for the new Account
The associated Contact must be valid (i.e., account.contact.valid?)
It looks like ActiveModel no longer includes the validates_associated method, as using that method results in an undefined method error. I considered requiring ActiveRecord::Validations, but this led down a stretch of various errors (e.g., undefined method `marked_for_destruction?')
I also considered defining validate on the Account class and calling valid? on the associated object, but that only prevented the form from submitting if there was also an error on the parent object.
validate do |account|
account.contact.valid?
# required for form to fail
errors.add(:base, "some error")
end
Is there something I'm not aware of to solve this? Thanks.
I recently (7 years after this question has been asked!) faced the same issue and solved it by implementing the AssociatedValidator based on the ActiveRecord one.
I simply included it in config/initializers folder:
module ActiveModel
module Validations
class AssociatedValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
if Array(value).reject { |r| valid_object?(r) }.any?
record.errors.add(attribute, :invalid, **options.merge(value: value))
end
end
private
def valid_object?(record)
record.valid?
end
end
module ClassMethods
def validates_associated(*attr_names)
validates_with AssociatedValidator, _merge_attributes(attr_names)
end
end
end
end
now you can use validates_associated in ActiveModel too.
class Person
include Virtus
include ActiveModel::Model
attribute :address, Address, :default => Address.new
validate :address_valid
private
def address_valid
errors.add(:base, 'address is not valid') unless address.valid?
end
end
class Address
include Virtus::ValueObject
include ActiveModel::Validations
attribute :line_1, String
attribute :line_2, String
validates :line_1, :presence => true
validates :line_2, :presence => true
end
The errors show up in the form if you pass an object to simple_fields_for:
= form.simple_fields_for person.address do |af|
= af.input :line_1
Another option is overriding valid?:
def valid?
super & address.valid?
end
Note its & not && so the conditions are not short circuited if the first returns false.

Are scopes in rails class or instance methods?

This is a question from a rails noob trying to understand concerns and scopes.
I always thought scopes were class methods in rails but then the other day I saw this code from DHH:
module Visible
extend ActiveSupport::Concern`
module ClassMethods
def visible_to(person)
where \
"(#{table_name}.bucket_id IN (?) AND
#{table_name}.bucket_type = 'Project') OR
(#{table_name}.bucket_id IN (?) AND
#{table_name}.bucket_type = 'Calendar')",
person.projects.pluck('projects.id'),
calendar_scope.pluck('calendars.id')
end
end
end
So the way the visible method is used is like so:
current_account.posts.visible_to(current_user)
This is what is confusing me. Self here is a collection of posts so we are acting on instances whereas the visible method seems to be meant to be used as a class method. Isn't david trying to call a class method as a dynamic scope? Could someone please clarify?
Class methods on classes which inherit ActiveRecord::Base can be used also as scopes (on ActiveRecord Relation objects).
Since the module Visible was meant to be mixed into a model which inherits ActiveRecord::Base, its class method visible_to can be used also as a scope.
If this did not clear the issue, you can implement a scope which gets all adult users (age > 20) in the following ways:
class User < ActiveRecord::Base
scope :adult, lambda { where("age > ?", 20) } # with a scope
class << self
def adult # with class method
where("age > ?", 20)
end
end
end
And use it exactly the same with User.adult

how to access a method in a model from a controller in rails

I want to put logic in the model rather than controller.
class UsersController < ApplicationController
def somemethod
d = User.methodinmodel
end
end
class User < ActiveRecord::Base
def methodinmodel
"retuns foo"
end
end
I get an error that there is no methodinmodel for the User model.
Why?
If you want to be able to call methodinmodel on the User class in general rather than a specific user, you need to make it a class method using self:
class User < ActiveRecord::Base
def self.methodinmodel
"returns foo"
end
end
Your current method definition would only work if you called it on a user:
#user = User.create!
#user.methodinmodel # Works.
User.methodinmodel # Doesn't work.
Using the new implementation using self would allow you to call it like:
User.methodinmodel # Works.

Adding the sum of a has_many relationship to a model's attributes

I have the following (simplified model) and wish to access the 'spent' value in the to_json method of the object, ideally as an attribute of the object.
class Task < ActiveRecord::Base
has_many :hours
def spent
self.hours.sum(:spent)
end
end
Is there a way to do this without defining a method and hacking the to_json method? I've been hunting for a way to use scope or by hacking the after_initialize method, but none of these methods provide a 'spent' value when using inspect or to_json on the model.
I need to solve this on models higher up the tree that use a has_many, through relationship too.
You can use the :methods parameter to to_json call.
object.to_json(:methods => :spent)
http://api.rubyonrails.org/classes/ActiveModel/Serializers/JSON.html
The best solution I could find to this was overriding the 'as_json' method to include the attributes that I needed, or to perform the logic required, (in this case the sum of the has_many relationship).
class Task < ActiveRecord::Base
...
def as_json(options = { })
options = {} if options.nil?
self[:job_id] = self.phase.job_id
self[:spent] = self.hours.sum(:spent)
super(options)
end
end