I've written a simple service to create the data I want to send over my ActionCable connection, but judging from the development log it looks like it is never executed and I can't figure out why.
#app\channels\quiz_data_channel.rb:
class QuizDataChannel < ApplicationCable::Channel
def subscribed
stream_from specific_channel
end
def send_data
logger.debug "[AC] send data method entered" #<-- this part is executed
QuizDataCreation.new(user: current_user).create #<-- calling the service HERE
logger.debug "[AC] service created new quiz data" #<-- not executed
#after I fix this issue I would broadcast the data here
end
private
def specific_channel
"quiz_data_#{params[:access_key]}"
end
end
#app\services\quiz_data_creation.rb:
module Services
class QuizDataCreation
def initialize(user)
self.user = user
logger.debug "[AC] service - initialize user: #{self.user.inspect}" #<-- not executed
end
def create
logger.debug "[AC] Service entered!" #<-- not executed
end
end
end
Calling the send_data method works, and the first text ("[AC] send data method entered") is printed into the development log, but that's it. I've been looking at several tutorials and tried placing the service in a subfolder of models at first, and calling Services::QuizDataCreation.new(user: current_user).create , but haven't gotten it to work yet. I'm sure there is a pretty obvious solution, but I just can't see it so I would be really thankful for any pointers.
Try defining the service class without module names Services as below:
#app\services\quiz_data_creation.rb:
class QuizDataCreation
def initialize(user)
self.user = user
logger.debug "[AC] service - initialize user: #{self.user.inspect}"
end
def create
logger.debug "[AC] Service entered!"
end
end
And then, you can access it normally as:
QuizDataCreation.new(current_user).create
Related
I am struggling to call a method in my User Model. I have a long method similar to this:
def find_content
def find_x
# call api
end
def find_y
#call api
end
content = {"x": find_x, "y": find_y}
return content
end
I then try to call it like this in my model:
class User < ApplicationRecord
def User.news
# get result of find_content
content = find_content
# I also tried doing User.find_content when the function was inside the model
## the function then passes the content variable to my UserMailer which sends emails to my users with the content
end
I have tried placing my find_content in the User Model with def self.find_content and without the self part.
I was wondering where is the best place to put a function that can be used like this in the model.
If I where are you I would create a Service class or a lib class, and I would call it.
Don't define your methods inside methods. Try something like this
class MyFancyService
def find_content
{"x": find_x, "y": find_y}
end
private
def find_x
#code
end
def find_y
#code
end
end
And inside your model
#remember to require your libs/services class in somewhere (maybe application.rb)
class User < ApplicationRecord
def news
MyFancyService.new.find_content
end
end
Do not abuse of Class method (def self.bla) you should have more instance methods.
The reason you are having this issue is that finding content is not really a User concern and should be broken out into a separate class as Horacio mentions, but I don't think the User class needs to know anything about finding content. It is possible that you need some User info to properly find content however.
I would suggest something like this (assuming you need something from a User object to call your api)
class User
def user_stuff_needed_by_api
end
end
class NewsAPI
def initialize(user_stuff)
# set stuff needed based on the user
end
def find_x
# call api
"x"
end
def find_y
# call api
"y"
end
def find_content
{"x": find_x, "y": find_y}
end
end
Then in your controller you have the user object, so get what you need from it, create an instance of the api and make your call
user_stuff = #user.user_stuff_needed_by_api
news_api = NewsAPI.new(user_stuff)
content = news_api.find_content
If you really want to make calls to the api inside your a User instance which I don't think you should, I would recommend passing in an instance of the api via a setter and then delegate find_content to that instance. So something like this.
class User
def set_news_api(api)
#news_api = api
end
def find_content
#news_api.find_content
end
end
Lastly if you really want to put all this in the User class something like this should work, but again is not recommended.
class User
def self.find_x
"xx"
# call api
end
def self.find_y
"yy"
#call api
end
def find_content
{"x": self.class.find_x, "y": self.class.find_y}
end
def self.other_find_content
{"other_x": find_x, "other_y": find_y}
end
def user_stuff_needed_by_api
end
end
puts User.new.find_content
puts User.other_find_content
I'm trying to move some business logic out of one of my controllers, StoreController and into a new Store::CreateService service object. Learning about services recently, it doesn't seem to be much of an established pattern for implementing them. I'm running into an error trying to call a protected method. I can obviously move the logic from those protected methods directly into execute but my understanding was that this should be okay to Rails.
undefined local variable or method `find_and_set_account_id' for #<Store::CreateService:0x00007f832f8928f8>
Here is the Service object
module Store
class CreateService < BaseService
def initialize(user, params)
#current_user, #params = user, params.dup
end
def execute
#store = Store.new(params)
#store.creator = current_user
find_and_set_account_id
if #store.save
# Make sure that the user is allowed to use the specified visibility level
#store.members.create(
role: "owner",
user: current_user
)
end
after_create_actions if #store.persisted?
#store
end
end
protected
def after_create_actions
event_service.create_store(#store, current_user)
end
def find_and_set_account_id
loop do
#store.account_id = SecureRandom.random_number(10**7)
break unless Store.where(account_id: account_id).exists?
end
end
end
You have an extra end after def execute..end. That end closes the CreateService class. This means your protected methods are defined on the Store module.
Hence the missing method.
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.
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
There's a controller action in my Rails app that contacts a user via text-message and email. For reasons I won't go into, the text-message needs to complete before the email can be sent successfully. I originally had something like this:
controller:
class MyController < ApplicationController
def contact_user
ContactUserWorker.perform_async(#user.id)
end
end
workers:
class ContactUserWorker
include Sidekiq::Worker
def perform(user_id)
SendUserTextWorker.perform_async(user_id)
SendUserEmailWorker.perform_async(user_id)
end
end
class SendUserTextWorker
include Sidekiq::Worker
def perform(user_id)
user = User.find(user_id)
user.send_text
end
end
class SendUserEmailWorker
include Sidekiq::Worker
def perform(user_id)
user = User.find(user_id)
user.send_email
end
end
This was unreliable; sometimes the email would fail, sometimes both would fail. I'm trying to determine whether perform_async was the cause of the problem. Was the async part allowing the email to fire off before the text had completed? I'm a little fuzzy on how exactly perform_async works, but that sounded like a reasonable guess.
At first, I refactored ContactUserWorker to:
class ContactUserWorker
include Sidekiq::Worker
def perform(user_id)
user = User.find(user_id)
User.send_text
SendUserEmailWorker.perform_async(user_id)
end
end
Eventually though, I just moved the call to send_text out of the workers altogether and into the controller:
class MyController < ApplicationController
def contact_user
#user.send_text
SendUserEmailWorker.perform_async(#user.id)
end
end
This is a simplified version of the real code, but that's the gist of it. It seems to be working fine now, though I still wonder whether the problem was Sidekiq-related or if something else was going on.
I'm curious whether my original structure would've worked if I'd used perform instead of perform_async for all the calls except the email call. Like this:
class MyController < ApplicationController
def contact_user
ContactUserWorker.perform(#user.id)
end
end
class ContactUserWorker
include Sidekiq::Worker
def perform(user_id)
SendUserTextWorker.perform(user_id)
SendUserEmailWorker.perform_async(user_id)
end
end
If the email can only be sent after the text message has been sent, then send the email after successful completion of sending the text.
class ContactUserWorker
include Sidekiq::Worker
def perform(user_id)
SendUserTextWorker.perform_async(user_id)
end
end
class SendUserTextWorker
include Sidekiq::Worker
def perform(user_id)
user = User.find(user_id)
text_sent = user.send_text
SendUserEmailWorker.perform_async(user_id) if text_sent
end
end
class SendUserEmailWorker
include Sidekiq::Worker
def perform(user_id)
user = User.find(user_id)
user.send_email
end
end
In user.send_text you need to handle the fact that neither the text or the email has been sent.
I'm curious whether my original structure would've worked if I'd used perform instead of perform_async for all the calls except the email call
It would have. But this is not what you actually intdending. What you really want is this:
class MyController < ApplicationController
def contact_user
ContactUserWorker.perform_async(#user.id)
end
end
class ContactUserWorker
include Sidekiq::Worker
attr_reader :user_id
def perform(user_id)
#user_id = user_id
user.send_text
user.send_email
end
def user
#user ||= User.find user_id
end
end
The problem was indeed the perform async part. It schedules both tasks to be executed in the background by a separate sidekiq daemon process. i guess your sidekiq is configured to execute the jobs concurrently. In the first version you've first scheduled the ContactUserWorker to perform it's job in a background outside of the current rails request. As this worker is startet later on, it kicks off two separate delayed workers in turn, which are then run in parallel and so there is no way to determine which of the both executes/finishes first.
I don't know what you mean exatly by sending text, but sending an email is an io blocking process and therefore it was a good idea to perform this in a background, because it would be blocking a complete rails process otherwise until the email is delivered (on a typical unicorn/passenger multi-process deployment). And as you actually want to execute both tasks sequentially and as an atomic operation, it's totally fine, performing them by a single sidekiq job/worker.
You also don't have to check if send_text succeeds. Sidekiq will retry the complete job if any part of it fails with an exception