Right now I have a model Trip that when saved, runs some callbacks. I want to isolate this behavior so that it happens only when it runs on the controller (create/update/destroy), so I want to remove the callbacks.
Assuming I have a service object DoSomething#call which accepts trip and does everything I need, what are my options to run it in create/update/destroy?
I have a few ideas but they involve horror things like:
def create
super() do |success, failure|
#action_successful = failure.instance_of?(
InheritedResources::BlankSlate
) || failure.class.nil?
if #action_successful
DoSomething.call(Trip.find(params[:id]))
end
end
end
Which comes with a lot of downsides:
The horrible way to detect if an action is successful
No way to get the in-memory reference to the object being acted on (reload from db)
Since I have no reference to the in memory object, it's quite problematic to run something during on destroy (no reference, can't reload)
Additional code as requested
class Trip
end
The custom service (I've multiples)
class SaveLastChangedTrip
def call(user, trip)
return if user.nil?
user.update_attributes!(last_trip: trip)
end
end
and the activeadmin file
ActiveAdmin.register Trip do
controller do
def update
if super() # This is pseudocode, I want to run this branch only if save is successful
SaveLastChangedTrip.call(current_user, resource)
end
end
end
end
I think you are looking for something like this:
def create
create! do |success, failure|
success.html do
DoSomething.call(resource)
end
end
end
See comments.rb for similar example.
Related
I am trying to call 'checking' action from post model.
class Post < ApplicationRecord
after_commit :testing
def testing
#id = "#{self.id}"
puts #id
checking # call action to controller
end
end
posts_controller.rb
def checking
puts "not Working"
end
I am trying to achieve the above, whereas it is not getting called.
You can do this by instantiating the controller.
SomeController.new.some_action
However it is Not Recommended as your model logic should be independent of your controller logic. Your model should follow Single-responsibility Principle.
A class should have only a single responsibility.
You might need some common logic for controller and models or some service!!!!
As it was said: the models should not know about controller.
Depending on the reason,
you can define checking in model and from Controller just call this method for current Post.
if something should be set from Controller and checked in Model , you can use the approach defined here
As many (or all) other said, you shouldn't call a Controller action from a Model. If you are saving/destroying and object from a Controller and you want to check is commit was called, you can do it in the controller, after saving or destroying.
posts_controller.rb
def create
#post = current_user.posts.build(post_params)
if #post.save
checking
end
end
def checking
puts "Is is working"
end
If this is not what you want, because you are specifically interested in the commit callback, you can change the code to this:
posts_controller.rb
def create
#post = current_user.posts.build(post_params)
#post.save
if #post.commmited?
checking
end
end
def checking
puts "Is is working"
end
And add some logic to your Model:
class Post < ApplicationRecord
attr_accessor :commit_performed
#unset the commit_performed attribute on first callbacks
before_destroy :unset_commit
before_validation :unset_commit
#set the commit_performed attribute after commit
after_commit :set_commit
def unset_commit
#commit_performed = false
end
def set_commit
#commit_performed = true
end
def commited?
#commit_performed
end
end
You should not be calling a controller action from a model. That's not how even controller methods are called. If you want to trigger an action, it should be either written as a target of a view form or input method, or as a redirect through another controller method itself. If you really want this action to be triggered (if you want the message after a record is saved to a listener url), I'd suggest using a lib such as HTTP::Net or a gem like HTTParty to trigger a call to the action, with its url or rails url_helper. That too, is not suggested, and/or is not the way to operate things in rails.
I have a simple helper in my rails app that sets a css class based on the state of an object, like:
<li class='<%= car_color(car.state) %>'><%= car.model %></li>
in the helper it's basically:
module CarHelper
def car_color(state)
if state == 'in service'
'car car-in-service'
elsif state == 'in garage'
'car car-in-garage'
else
'car'
end
end
end
And it works fine for my usecase. However, how there's a new requirement that a User with a role of customer should not see the color coding that this helper creates.
My first thought was to do a check in the controller to see if the user should be able to see the color coding:
class CarsController < ApplicationController
before_action :is_customer?
# bunch of restful stuff
private
def is_customer?
#customer if current_user.roles.include? 'customer'
end
end
And then in my helper, I can just add a line:
def car_color(color)
return 'car' if #customer
end
This meets the requirements, but it smells to me. Now my helper has a dependency on #customer which is passed simply because it is an instance variable. An alternative would be to explicitly pass in a user.role to car_color or to wrap all of the calls to car_color in conditionals based on the user.role, which seems even worse.
Is there a way to help prepare this code for even more conditionals based on different user roles? My thought is to do something like:
module CarHelper
def car_color(args)
set_color_for_role(args[:user_role]) if args[:user_role]
set_color_for_state(args[:car_state]) if args[:car_state]
end
private
def set_color_for_role(user_role)
# stuff
end
def set_color_for_state(car_state)
# stuff
end
end
I don't use rails helpers very often since I mostly work in angular and am wondering if I'm missing a cleaner OOP approach.
I don't see any issue with checking the current user's roles in the helper method.
You could move the checking behaviour to the user model though which would make things cleaner (you may of course want to generalise this for multiple roles):
class User < ActiveRecord::Base
def is_customer?
self.roles.include?('customer')
end
end
Then in your helper you can just check if current_user.is_customer?
def car_color(state)
if current_user.is_customer?
'car'
else
if state == 'in service'
'car car-in-service'
elsif state == 'in garage'
'car car-in-garage'
else
'car'
end
end
I find it useful sometimes to build up an array of the classes too which is often cleaner (I've thrown in a case too):
def car_color(state)
car_classes = ['car']
unless current_user.is_customer?
car_classes << case state
when 'in service' then 'car-in-service'
when 'in garage' then 'car-in-garage'
end
end
car_classes.join(" ")
end
Use the draper https://github.com/drapergem/draper gem and move this logic to decorator
I have a Rails 3.2 app. It is a publishing app where we kick off several Sidekiq jobs in response to changes in content. I was calling this from the controller but there's now getting to be multiple points of entry and are now duplicating logic in multiple controllers. The proper place for this to be is in a callback in the model. However, accessing current_user is frowned upon in the model but for things like logging changes or app events, it is critical.
So I have two questions (1) Is there something I'm missing regarding the argument about accessing current_user when you want to be logging changes across complex model structures? and (2) Is the proposed solution here an effective one with last update over 2 years ago in terms of thread-safety? I use a three Unicorn processes on Heroku. https://stackoverflow.com/a/2513456/152825
Edit 1
Thinking through this, wondering if I should just do something like this in my application.rb
class ArcCurrentUser
#current_user_id
def self.id
return #current_user_id
end
def self.id=id_val
#current_user_id=id_val
end
end
and then in my current_user method in application_controller, just update ArcCurrentUser.id to #current_user.id? I will only be using it for this logging functionality.
You're correct in that you can't access current_user from a model.
As for the answer you linked, I'm not entirely sure but I think it's not fully thread-safe. From the same question, I like this answer https://stackoverflow.com/a/12713768/4035338 more.
Say we have a controller with this action
...
def update
#my_object = MyModel.find(params[:id])
#my_object.current_user = current_user
#my_object.assign_attributes params[:my_model]
#my_object.save
end
...
and this model
class MyModel < ActiveRecord::Base
attr_accessor :current_user
before_save :log_who_did_it
private
def log_who_did_it
return unless current_user.present?
puts "It was #{current_user}!"
end
end
Or my favourite
...
def update
#my_object = MyModel.find(params[:id])
#my_object.update_and_log_user(params[:my_model], current_user)
end
...
class MyModel < ActiveRecord::Base
...
def update_and_log_user(params, user)
update_attributes(params)
puts "It was #{user}!" if user.present?
end
end
So I apologize for how noobish these questions may seem. I'm new to rails and as a first task I also brought in Neo4J as it seemed like the best fit if I grow the project.
I'll explain the flow of actions then show some example code. I'm trying to add in step 3-5 now.
User logs in via FB
The first login creates a user node. If the user exist, it simply retrieves that user+node
After the user node is created, the koala gem is used to access the FB Graph API
Retrieves friendlist of each friend using the app.
Go through each friend and add a two way friendship relationship between the two users
As 3-5 only needs to happen when the user first joins, I thought I could do this in a method associated with after_save callback. There is a flaw to this logic though as I will need to update the user at some point with additional attributes and it will call after_save again. Can I prevent this from occurring with update?
SessionsController for reference
def create
user = User.from_omniauth(env["omniauth.auth"])
session[:user_id] = user.id
redirect_to root_url
end
def destroy
session.delete(:user_id)
redirect_to root_path
end
So in my user.rb I have something like this
has_many :both, :friendships
after_save :check_friends
def self.from_omniauth(auth)
#user = User.where(auth.slice(:provider, :uid)).first
unless #user
#user = User.new
# assign a bunch of attributes to #user
#user.save!
end
return #user
end
def facebook
#facebook ||= Koala::Facebook::API.new(oauth_token)
block_given? ? yield(#facebook) : #facebook
rescue Koala::Facebook::APIError => e
logger.info e.to_s
nil
end
def friends_count
facebook { |fb| fb.get_connection("me", "friends", summary: {}) }
end
def check_friends(friendships)
facebook.get_connection("me", "friends").each do |friend|
friend_id = friend["id"]
friend_node = User.where(friend_id)
Friendship.create_friendship(user,friend_node)
return true
end
end
friendship.rb
from_class User
to_class User
type 'friendship'
def self.create_friendship(user,friend_node)
friendship = Friendship.create(from_node: user, to_node: friend_node)
end
I'm not sure if I'm on the right track with how to create a relationship node. As I just created #user, how do I incorporate that into my check_friends method and retrieve the user and friend node so properly so I can link the two together.
Right now it doesn't know that user and friend_user are nodes
If you see other bad code practice, please let me know!
In advance: Thanks for the help #subvertallchris. I'm sure you will be answering lots of my questions like this one.
This is a really great question! I think that you're on the right track but there are a few things you can change.
First, you need to adjust that has_many method. Your associations always need to terminate at a node, not ActiveRel classes, so you need to rewrite that as something like this:
has_many :both, :friends, model_class: 'User', rel_class: 'Friendship'
You'll run into some problems otherwise.
You may want to consider renaming your relationship type in the interest of Neo4j stylistic consistency. I have a lot of bad examples out there, so sorry if I gave you bad ideas. FRIENDS_WITH would be a better relationship name.
As for handling your big problem, there's a lot you can do here.
EDIT! Crap, I forgot the most important part! Ditch that after_save callback and make the load existing/create new user behavior two methods.
class SessionsController < ApplicationController
def create
user = User.from_omniauth(env["omniauth.auth"])
#user = user.nil? ? User.create_from_omniauth(env["omniauth.auth"]) : user
session[:user_id] = #user.id
redirect_to root_url
end
def destroy
session.delete(:user_id)
redirect_to root_path
end
end
class User
include Neo4j::ActiveNode
# lots of other properties
has_many :both, :friends, model_class: 'User', rel_class: 'Friendship'
def self.from_omniauth(auth)
User.where(auth.slice(:provider, :uid)).limit(1).first
end
def self.create_from_omniauth(auth)
user = User.new
# assign a bunch of attributes to user
if user.save!
user.check_friends
else
# raise an error -- your user was neither found nor created
end
user
end
# more stuff
end
That'll solve your problem with getting it started. You may want to wrap the whole thing in a transaction, so read about that in the wiki.
But we're not done. Let's look at your original check_friends:
def check_friends(friendships)
facebook.get_connection("me", "friends").each do |friend|
friend_id = friend["id"]
friend_node = User.where(friend_id)
Friendship.create_friendship(user,friend_node)
return true
end
end
You're not actually passing it an argument, so get rid of that. Also, if you know you're only looking for a single node, use find_by. I'm going to assume there's a facebook_id property on each user.
def check_friends
facebook.get_connection("me", "friends").each do |friend|
friend_node = User.find_by(facebook_id: friend["id"])
Friendship.create_friendship(user,friend_node) unless friend_node.blank?
end
end
The create_friendship method should should return true or false, so just make that the last statement of the method does that and you can return whatever it returns. That's as easy as this:
def self.create_friendship(user, friend_node)
Friendship.new(from_node: user, to_node: friend_node).save
end
create does not return true or false, it returns the resultant object, so chaining save to your new object will get you what you want. You don't need to set a variable there unless you plan on using it more within the method.
At this point, you can easily add an after_create callback to your ActiveRel model that will do something on from_node, which is always the User you just created. You can update the user's properties however you need to from there. Controlling this sort of behavior is exactly why ActiveRel exists.
I'd probably rework it a bit more, still. Start by moving your facebook stuff into a module. It'll keep your User model cleaner and more focused.
# models/concerns/facebook.rb
module Facebook
extend ActiveSupport::Concern
def facebook
#facebook ||= Koala::Facebook::API.new(oauth_token)
block_given? ? yield(#facebook) : #facebook
rescue Koala::Facebook::APIError => e
logger.info e.to_s
nil
end
def friends_count
facebook { |fb| fb.get_connection("me", "friends", summary: {}) }
end
end
# now back in User...
class User
include Neo4j::ActiveNode
include Facebook
# more code...
end
It's really easy for your models to become these messy grab bags. A lot of blogs will encourage this. Fight the urge!
This should be a good start. Let me know if you have any questions or if I screwed anything up, there's a lot of code and it's possible I may need to clarify or tweak some of it. Hope it helps, though.
My question is about controller methods (possibly included from an outside class) that work with instance variables. I frequently use a before_filter in controllers to set up certain variables, e.g.:
class DocumentController < ApplicationController
before_filter :fetch_document
def action
#document.do_something
end
private
def fetch_document
#document = Document.find(params[:id])
end
end
I've been working on a project in which a few controllers will share some functionality, say, document editing. My first thought was to extract the relevant methods, and get them from application_controller.rb or a separate module. But then I noticed I was writing code that looks like this:
def fetch_document
#document = Document.find(params[:id])
end
def do_something_to_document
#document.do_something
end
This sets off warning bells: do_something_to_document is essentially assuming the existence of #document, rather than taking it as an argument. Is this, in your sage opinions, a bad coding practice? Or am I being paranoid?
Assuming it is an issue, I see two general approaches to deal with it:
Check for the instance var and bail unless it's set:
def do_something_to_document
raise "no doc!" unless #document
[...]
end
Call the action with the instance var as an argument:
def do_something_to_document(document)
[...]
end
2 looks better, because it hides the context of the calling object. But do_something_to_doc will only be called by controllers that have already set up #document, and taking #document as a method argument incurs the overhead of object creation. (Right?) 1 seems hackish, but should cover all of the cases.
I'm inclined to go with 1 (assuming I'm right about the performance issue), even though seeing a list of methods referencing mysterious instance vars gives me hives. Thoughts? Let me know if I can be more clear. (And of course, if this is answered somewhere I didn't see it, just point me in the right direction...)
Thanks,
-Erik
If you really need document in different controllers, I'd do something like this:
class ApplicationController < ActionController::Base
private
def document
#document ||= Document.find(params[:document_id])
end
end
class FooController < ApplicationController
before_filter :ensure_document, :only => [:foo]
def foo
document.do_something
end
private
# TODO: not sure if controller_name/action_name still exists
def ensure_document
raise "#{controller_name}##{action_name} needs a document" unless document
end
end
As #variable are session/instance variable you will get a Nil exception in do_something_to_document method.
The first code is fine, because before_filter will always load your #document.
I suggest you to write something like that
def fetch_document(doc_id)
#document ||= Document.find(doc_id)
end
def do_something_to_document
my_doc = fetch_document(params[:id])
end
where do_something_to_document is in the controller (if not, dont use params[:id], even if you know you can access this global, use another explicit parameter). The ||= thing, will asssure that you call the base only once by request.