In a Rails project because of some history reasons, a piece of codes in Tiger and Elephant are same.
I don't like the repetition, but if I create a new method in AnimalController class and move these codes into it, I can't return the walk or running method from the new method.
I think return from another method may not a good practice, but I really hate the duplications, can someone help me refactoring?
class AnimalController
# I want create a new method here
#def all_in
#end
end
class TigerController < AnimalController
def running # This is an Action
some_different_codes...
if arm.blank?
render_not_found
return # <- how can I return `running` from the new method?
end
if lag.nil?
invalid_id
return # <-
end
some_different_codes...
end
end
class ElephantController < AnimalController
def walk # This is an Action
some_different_codes...
if arm.blank?
render_not_found
return
end
if lag.nil?
invalid_id
return
end
some_different_codes...
end
end
A method can't make its caller return, if it doesn't want to. So this new method will perform checks (while rendering something) and it will return result of the checks. Caller method analyzes the return value and decides what to do. Something along these lines:
class AnimalController
def all_in
if invalid_id
render_not_found
return false
end
if lag.nil?
invalid_id
return false
end
true
end
end
class TigerController < AnimalController
def running # This is an Action
some_different_codes...
return unless all_in
some_different_codes...
end
end
Might be a good turn to look up about callbacks and superclassing:
Callbacks basically allow you to run login in code based on the response of another function.
Since we have them all over Rails, not many people actually appreciate what they do. If you've ever implemented them in JS, you'll know all about them!
-
Superclassing is where you inherit from an existing class - allowing you to use (& extend) the functions the class has. This is where the super command comes from.
I'd do this (looks like Sergio's answer actually, which is reassuring):
#app/controllers/animals_controller.rb
class AnimalsController < ApplicationController
private
def all_in?
if invalid_id
return false
end
if lag.nil?
invalid_id
return false
end
true #-> ruby automatically returns the last line
end
end
The above is what you'd call a callback -- you'll be able to call all_in (in an instance) and receive a response of either true or false.
This will give you the ability to call that method (if you're superclassing, the method will be available down the chain:
#app/controllers/elephants_controller.rb
class ElephantController < AnimalController
def walk # This is an Action
some_different_codes...
if all_in?
some_different_codes...
end
end
end
Now, there's something you must be aware of.
This type of behavior should not be put in the controller of your app - it should be in your models:
#app/models/animal.rb
class Animal < ActiveRecord::Base
def walk
end
end
#app/models/animals/elephant.rb
class Elephant < Animal
def walk
super ...
end
end
#app/models/animals/tiger.rb
class Tiger < Animal
end
The above is known as STI (Single Table Inheritance). It's basically a way to subclass against a main model with other "dependent" models & their methods.
Since ruby is object orientated, you should be calling object-specific methods on the objects themselves;
#config/routes.rb
resources :tigers, :elephants, controller: :animals
#url.com/tigers/
#url.com/elephants/
#app/controllers/animals_controller.rb
class AnimalsController < ApplicationController
def show
#tiger = Tiger.find params[:id]
#tiger.walk
end
end
This is more or less epitomized with a state machine:
class Vehicle
attr_accessor :seatbelt_on, :time_used, :auto_shop_busy
state_machine :state, :initial => :parked do
before_transition :parked => any - :parked, :do => :put_on_seatbelt
after_transition :on => :crash, :do => :tow
after_transition :on => :repair, :do => :fix
after_transition any => :parked do |vehicle, transition|
vehicle.seatbelt_on = false
end
...
Related
I am on rails 4.2.10. I need to trigger a job using sidekiq in after_save method. But the job is triggered, before the object is committed into the database, so I get the error, object not found with id=xyz.
So, I need to use
after_commit :method_name, :on => [:create, :update]
But the changes that I made in object doesn't show up in above method. I have an attribute email. When I was calling above method after_save, email_changed? return true. But if I call the same method using after_commit, email_changed? returns `false.
Is it because I am using object.save method and not create method?
Below is the method, which I am calling to trigger the job:
def update_or_create_user
if email_changed?
ServiceUpdateDataJob.perform_later action: 'update', data: {type: 'user', user_id: self.id}
end
true
end
I recognize this isn't exactly an answer to your question as stated. However...
IMO, you're overloading your model's responsibilities. I suggest you create a service that triggers the job when your model is saved. It might look something like:
class FooService
attr_accessor :unsaved_record
class << self
def call(unsaved_record)
new(unsaved_record).call
end
end
def initialize(unsaved_record)
#unsaved_record = unsaved_record
end
def call
kick_off_job if unsaved_record.save
!unsaved_record.new_record?
end
private
def kick_off_job
# job logic
end
end
You might use the service in a controller something like:
class FooController < ApplicationController
def create
#new_record = ModelName.new(record_params)
if FooService.call(#new_record)
# do successful save stuff
else
# do unsuccessful save stuff
end
end
...
end
I've got a sidekiq job that needs to be run after the commit, but only in some situations and not all, in order to avoid a common race condition.
For example, the below after_commit will always fire but the code inside will only execute if the flag is true (previously set in the verify method).
class User < ActiveRecord::Base
...
after_commit do |user|
if #enqueue_some_job
SomeJob.new(user).enqueue
#enqueue_some_job = nil
end
end
def verify
#enqueue_some_job = ...
...
save!
end
end
The code is a bit ugly. I'd much rather be able to somehow wrap the callback inline like this:
class User < ActiveRecord::Base
def verify
if ...
run_after_commit do |user|
SomeJob.new(user).enqueue
end
end
...
save!
end
end
Does anything built into Rails exist to support a syntax like this (that doesn't rely on setting a temporary instance variable)? Or do any libraries exist that extend Rails to add a syntax like this?
Found a solution using a via a concern. The snippet gets reused enough that it is probably a better option to abstract the instance variable and form a reusable pattern. It doesn't handle returns (not sure which are supported via after_commit since no transaction is present to roll back.
app/models/concerns/callbackable.rb
module Callbackable
extend ActiveSupport::Concern
included do
after_commit do |resource|
if #_execute_after_commit
#_execute_after_commit.each do |callback|
callback.call(resource)
end
#_execute_after_commit = nil
end
end
end
def execute_after_commit(&callback)
if callback
#_execute_after_commit ||= []
#_execute_after_commit << callback
end
end
end
app/models/user.rb
class User < ActiveRecord::Base
include Callbackable
def verify
if ...
execute_after_commit do |user|
SomeJob.new(user).enqueue
end
end
...
save!
end
end
You can use a method name instead of a block when declaring callbacks:
class User < ActiveRecord::Base
after_commit :do_something!
def do_something!
end
end
To set a condition on the callback you can use the if and unless options. Note that these are just hash options - not keywords.
You can use a method name or a lambda:
class User < ActiveRecord::Base
after_commit :do_something!, if: -> { self.some_value > 2 }
after_commit :do_something!, unless: :something?
def do_something!
end
def something?
true || false
end
end
Assuming that you need to verify a user after create.
after_commit :run_sidekiq_job, on: :create
after_commit :run_sidekiq_job, on: [:create, :update] // if you want on update as well.
This will ensure that your job will run only after a commit to db.
Then define your job that has to be performed.
def run_sidekiq_job
---------------
---------------
end
Hope it helps you :)
I have two models, restaurant and cuisine with a many to many association. And I have this in my app/admin/restaurant.rb
ActiveAdmin.register Restaurant do
scope("All"){|scope| scope.order("created_at desc")}
Cuisine.all.each do |c|
scope(c.name) { |scope| scope.joins(:cuisines).where("cuisines.id=?",c.id)}
end
end
The problem is whenever I delete or add a new cuisine the scopes do not change until I make a change to my admin/restaurant.rb file. How can I fix this issue?
I was able to fix this by adding in my admin/restaurant.rb
controller do
before_filter :update_scopes, :only => :index
def update_scopes
resource = active_admin_config
Cuisine.order("created_at ASC").each do |m|
next if resource.scopes.any? { |scope| scope.name == m.name}
resource.scopes << (ActiveAdmin::Scope.new m.name do |restaurants|
restaurants.joins(:cuisines).where("cuisines.id=?", m.id)
end)
end
resource.scopes.delete_if do |scope|
!(Cuisine.all.any? { |m| scope.name == m.name })
end
resource.scopes.unshift(ActiveAdmin::Scope.new "All" do |restaurants| restaurants end)
end
found the solution here
I'm not sure of a way to defines scopes dynamically, at least using the scope method.
The alternative to the scope method is defining a class method, which accomplishes the same thing so far as I know.
In other words,
scope("All"){|scope| scope.order("created_at desc")}
is the same as
# in a Class
class << self
def All
order("created_at desc")
end
end
You can dynamically create class methods using this method (taken from ruby-defining-class-methods:
class Object
def meta_def name, &blk
(class << self; self; end).instance_eval { define_method name.to_s, &blk }
end
end
I'll use the following to remove the generated class methods:
class Object
def meta_undef name
(class << self; self; end).class_eval { remove_method name.to_sym }
end
end
These methods can be called from the save and destroy hooks on your models, i.e.:
# in a Model
def save(*args)
self.class.meta_def(name) do
joins(:cuisines).where("cuisines.id=?",c.id)
end
super(*args)
end
def destroy(*args)
self.class.meta_undef(name)
super(*args)
end
Then whenever a record is created or removed, the scopes will be updated. There are pros and cons of this approach. Clearly it's nice to define methods on the fly, but this is vulnerable to remote code execution.
Personally I'd probably hold off from dynamically defining class methods (i.e. scopes) and just make one that accepts an argument. Example:
# This is with standard ActiveRecord, not sure about ActiveAdmin
class Restaurant < ActiveRecord::Base
def self.All
order("created_at desc")
end
end
class Cuisine < ActiveRecord::Base
def self.by_name(name)
Restaurant.all.joins(:cuisines).where("cuisines.name=?", name)
end
end
Cuisine.by_name("something")
Restaurant.all.All
Restaurant.All
edit in response to your comment:
load(file) will re-load the source. So you could try the following:
# in a model
def save(*args)
load(Rails.root.join("app", "models", "THIS_MODEL_FILE.rb")
super
end
def destroy(*args)
load(Rails.root.join("app", "models", "THIS_MODEL_FILE.rb")
super
end
Under the hood, save is called for both create and update. So overriding it and destroy covers all the CRUD operations.
The reason I didn't initially recommend this approach is that I haven't personally used it. I'd be curious to know how it works.
I recently had a rails model that had several callbacks on it like so:
class Model < ActiveRecord::Base
before_validation :fetch_posts
after_create :build_posts
def fetch_posts
fetch_collection
rescue MyException => e
self.errors.add(:post, e.message)
end
def build_posts
fetch_collection.each do |item|
DifferentModel.build(item)
end
end
def fetch_collection
#collection ||= method_that_fetches_collection_from_external_source
end
end
This was working just fine but it was making it extremely difficult to write tests, as whenever I wanted to create a Model I had to stub out all the callbacks. Enter service objects:
class ModelFetcher
attr_reader :model
def initialize(model)
#model = model
end
def save
model.fetch_posts
if model.save
model.build_posts
return true
else
return false
end
end
end
The problem I'm seeing now, in the case where a model does indeed contain an error (from the fetch posts method), it doesn't get carried over to the model.save call in the SO. That is to say, the Model.new has an error, but once I call .save on that Model.new it doesn't maintain the error and the model saves properly.
I considered adding validate :fetch_posts but then I am back in the same situation I was before as this is essentially a callback.
Any advice on how to structure this better? Is it possible to maintain an error from Model.new to .save? Am I fundamentally misunderstanding something?
Thanks!
Here is an alternate solution which is to overwrite run_validations! since you have none.
class Model < ActiveRecord::Base
after_create :build_posts
def fetch_posts
fetch_collection
rescue MyException => e
self.errors.add(:post, e.message)
end
def build_posts
fetch_collection.each do |item|
DifferentModel.build(item)
end
end
def fetch_collection
#collection ||= method_that_fetches_collection_from_external_source
end
private
def run_validations!
fetch_posts
errors.empty?
end
end
Usually this method looks like
def run_validations!
run_callbacks :validate
errors.empty?
end
but since you have no validations it should serve a similar purpose on #save.
Or as I suggested in a comment you can replace save with model.errors.any? Since save will clear your original errors set by fetch_posts but errors.any? Will check if there were errors during the fecth_posts method.
I'm trying to do something like:
account.users << User.new
But I need users to be a method on an account. So I've tried things like:
def users<<(obj)
But I've had no luck with that. Is this even possible to do in Ruby? I would assume so because the ActiveRecord relationships seem to work this way in Rails.
Check this answer: Rails: Overriding ActiveRecord association method
[this code is completely from the other answer, here for future searchers]
has_many :tags, :through => :taggings, :order => :name do
def << (value)
"overriden" #your code here
end
end
It seems like you might not be describing your actual problem, but to answer your question -- yes you can override the << operator:
class Foo
def <<(x)
puts "hi! #{x}"
end
end
f = Foo.new
=> #<Foo:0x00000009b389f0>
> f << "there"
hi! there
I assume you have a model like this:
class Account < ActiveRecord::Base
has_and_belongs_to_many :users
end
To override Account#users<<, you need to define it in a block that you pass to has_and_belongs_to_many:
class Account < ActiveRecord::Base
has_and_belongs_to_many :users do
def <<(user)
# ...
end
end
end
You can access the appropriate Account object by referring to proxy_association.owner:
def <<(user)
account = proxy_association.owner
end
To call the original Account#users<<, call Account#users.concat:
def <<(user)
account = proxy_association.owner
# user = do_something(user)
account.users.concat(user)
end
For more details, see this page: Association extensions - ActiveRecord
In this case it's the << of you class of you User. So can be an Array or a AssociationProxy.
The must simplest is create a new method to do what you want.
You can override the method by instance instead.
account.users.instance_eval do
def <<(x)
put 'add'
end
end
account.users << User.new
# add
But you need do that all the time before you add by <<
users would return an object that has overridden << operator like Array, IO, String, or any type you create. You override like this:
class SomeType
def <<(obj)
puts "Appending #{obj}"
end
end
If you are trying to perform an action upon adding an User to the users collection, you can use association callbacks instead of over-riding <<(as there are many ways to add an object to an association).
class Account
has_many :users, :after_add => :on_user_add
def on_user_add(user)
p "Added user : #{user.name} to the account: #{name}"
end
end