I have a multitenant Rails app that has a tenant_id column on many models.
Each model that belongs to a specific tenant has a default scope based on a class variable on the Tenant class:
default_scope { where(tenant_id: Tenant.current_id) }
Tenant.current_id is set in the application controller.
The problem is that when I send mail (via a Delayed Job) regarding a tenant-scoped object (ie. UserMailer.delay.contact_user(#some_user_in_a_specific_tenant)), I get NoMethodErrors for nilClass whenever I call anything on #some_user_in_a_specific_tenant within the Mailer. Presumably because the Delayed Job process isn't setting the Tenant.current_id.
How can I get DJ to access the objects I'm passing in?
Grab the current_id when you queue the job and build a scope out of that that doesn't depend on a class variable from the app. Or get a list of record ids to operate on first, passing that to DJ.
Examples:
def method_one(id)
Whatever.where(:tenant_id => id).do_stuff
end
def method_two(ids)
Whatever.find(ids).do_stuff
end
handle_asynchronously :method_one, :method_two
# then
method_one(Tenant.current_id)
# or
ids = Whatever.all.map(&:id)
method_two(ids)
Related
Just having trouble figuring out how to test an action that utilizes a private ApplicationController method. To explain:
A User has and belongs to many Organisations (through Roles), and vice versa. An Organisation has a bunch of related entities, but in the app a user is only dealing with a single Organisation at a time, so for all of my controller methods I want to scope with a current organisation. So in my ApplicationController:
private
# Returns the organisation that is being operated on in this session.
def current_org
# Get the org id from the session. If it doesn't exist, or that org is no longer operable by the current user,
# find an appropriate one and return that.
if (!session[:current_organisation_id] || !current_user.organisations.where(['organisations.id = ?', session[:current_organisation_id]]).first)
# If an org doesn't exist for this user, all hope is lost, just go to the home page
redirect_to '/' if (!current_user.organisations.first)
# Otherwise set the session with the new org
session[:current_organisation_id] = current_user.organisations.first.id;
end
# Return the current org!
current_user.organisations.first
end
First things first, I don't know if this is the best way to scope. I'd love some elegant default_scope thing, but with the use of session variables this seems problematic. Anyway, the above let's me do this in controllers:
class HousesController < ApplicationController
def index
#houses = current_org.houses
end
end
So now I want to use rspec to test my controllers. How can I make sure the current_org method can be called and returns one of the factory_girl Organisation models so that I can write the spec. I'm just confused at how best to bring the factory, spec, action and AppControlled method together.
I have two models, User and PushupReminder, and a method create_a_reminder in my PushupReminder controller (is that the best place to put it?) that I want to have create a new instance of a PushupReminder for a given user when I pass it a user ID. I have the association via the user_id column working correctly in my PushupReminder table and I've tested that I can both create reminders & send the reminder email correctly via the Rails console.
Here is a snippet of the model code:
class User < ActiveRecord::Base
has_many :pushup_reminders
end
class PushupReminder < ActiveRecord::Base
belongs_to :user
end
And the create_a_reminder method:
def create_a_reminder(user)
#user = User.find(user)
#reminder = PushupReminder.create(:user_id => #user.id, :completed => false, :num_pushups => #user.pushups_per_reminder, :when_sent => Time.now)
PushupReminderMailer.reminder_email(#user).deliver
end
I'm at a loss for how to run that create_a_reminder method in my code for a given user (eventually will be in a cron job for all my users). If someone could help me get my thinking on the right track, I'd really appreciate it.
Thanks!
Edit: I've posted a sample Rails app here demonstrating the stuff I'm talking about in my answer. I've also posted a new commit, complete with comments that demonstrates how to handle pushup reminders when they're also available in a non-nested fashion.
Paul's on the right track, for sure. You'll want this create functionality in two places, the second being important if you want to run this as a cron job.
In your PushupRemindersController, as a nested resource for a User; for the sake of creating pushup reminders via the web.
In a rake task, which will be run as a cron job.
Most of the code you need is already provided for you by Rails, and most of it you've already got set in your ActiveRecord associations. For #1, in routes.rb, setup nested routes...
# Creates routes like...
# /users/<user_id>/pushup_reminders
# /users/<user_id>/pushup_reminders/new
# /users/<user_id>/pushup_reminders/<id>
resources :users do
resources :pushup_reminders
end
And your PushupRemindersController should look something like...
class PushupRemindersController < ApplicationController
before_filter :get_user
# Most of this you'll already have.
def index
#pushup_reminders = #user.pushup_reminders
respond_with #pushup_reminders
end
# This is the important one.
def create
attrs = {
:completed => false,
:num_pushups => #user.pushups_per_reminder,
:when_sent => Time.now
}
#pushup_reminder = #user.pushup_reminders.create(attrs)
respond_with #pushup_reminder
end
# This will handle getting the user from the params, thanks to the `before_filter`.
def get_user
#user = User.find(params[:user_id])
end
end
Of course, you'll have a new action that will present a web form to a user, etc. etc.
For the second use case, the cron task, set it up as a Rake task in your lib/tasks directory of your project. This gives you free reign to setup an action that gets hit whenever you need, via a cron task. You'll have full access to all your Rails models and so forth, just like a controller action. The real trick is this: if you've got crazy custom logic for setting up reminders, move it to an action in the PushupReminder model. That way you can fire off a creation method from a rake task, and one from the controller, and you don't have to repeat writing any of your creation logic. Remember, don't repeat yourself (DRY)!
One gem I've found quite useful in setting up cron tasks is the whenever gem. Write your site-specific cron jobs in Ruby, and get the exact output of what you'd need to paste into a cron tab (and if you're deploying via Capistrano, total hands-off management of cron jobs)!
Try setting your attr_accessible to :user instead of :user_id.
attr_accessible :user
An even better way to do this however would be to do
#user.pushup_reminders.create
That way the user_id is automatically assigned.
Use nested routes like this:
:resources :users do
:resources :pushup_reminders
end
This will give you params[:user_id] & params[:id] so you can find your objects in the db.
If you know your user via sessions, you won't need to nest your routes and can use that to save things instead.
Using restful routes, I would recommend using the create action in the pushup_reminders controller. This would be the most conventional and Restful way to do this kind of object creation.
def create
#user = User.find(params[:user_id]
#reminder = #user.pushup_reminders.create()
end
If you need to check whether object creation was successful, try using .new and .save
I have a method like this that goes through an array to find different APIs and launch a delayed_job instance for every API found like this.
def refresh_users_list
apis_array.each do |api|
api.myclass.new.delay.get_and_create_or_update_users
end
end
I have an after_filter on users#index controller to trigger this method. This is creating many jobs to be triggered that will eventually cause too many connections problems on Heroku.
I'm wondering if there's a way I can check for the presence of a Job in the database by each of the API that the array iterates. This would be very helpful so I can only trigger a particular refresh if that api wasn't updated on a given time.
Any idea how to do this?
In config/application.rb, add the following
config.autoload_paths += Dir["#{config.root}/app/jobs/**/"]
Create a new directory at app/jobs/.
Create a file at app/jobs/api_job.rb that looks like
class ApiJob < Struct.new(:attr1, :attr2, :attr3)
attr_accessor :token
def initialize(*attrs)
self.token = self.class.token(attr1, attr2, attr3)
end
def display_name
self.class.token(attr1, attr2, attr3)
end
#
# Class methods
#
def self.token(attr1, attr2, attr3)
[name.parameterize, attr1.id, attr2.id, attr3.id].join("/")
end
def self.find_by_token(token)
Delayed::Job.where("handler like ?", "%token: #{token}%")
end
end
Note: You will replace attr1, attr2, and attr3 with whatever number of attributes you need (if any) to pass to the ApiJob to perform the queued task. More on how to call this in a moment
For each of your API's that you queue some get_and_create_or_update_users method for you'll create another Job. For example, if I have some Facebook api model, I might have a class at app/jobs/facebook_api_job.rb that looks like
class FacebookApiJob < ApiJob
def perform
FacebookApi.new.get_and_create_or_update_users(attr1, attr2, attr3)
end
end
Note: In your Question you did not pass any attributes to get_and_create_or_update_users. I am just showing you where you would do this if you need the job to have attributes passed to it.
Finally, wherever your refresh_users_list is defined, define something like this job_exists? method
def job_exists?(tokens)
tokens = [tokens] if !tokens.is_a?(Array) # allows a String or Array of tokens to be passed
tokens.each do |token|
return true unless ApiJob.find_by_token(token).empty?
end
false
end
Now, within your refresh_users_list and loop, you can build new tokens and call job_exists? to check if you have queued jobs for the API. For example
# Build a token
def refresh_users_list
apis_array.each do |api|
token = ApiJob.token(attr1, attr2, attr3)
next if job_exists?(token)
api.myclass.new.delay.get_and_create_or_update_users
end
end
Note: Again I want to point out, you won't be able to just drop in the code above and have it work. You must tailor it to your application and the job's you're running.
Why is this so complicated?
From my research, there's no way to "tag" or uniquely identify a queued job through what delayed_job provides. Sure, each job has a unique :id attribute. You could store the ID values for each created job in some hash somewhere
{
"FacebookApi": [1, 4, 12],
"TwitterApi": [3, 193, 44],
# ...
}
and then check corresponding hash key for an ID, but I find this limiting, and not always sufficient for the problem When you need to identify a specific job by multiple attributes like above, we must create a way to find these jobs (without loading every job into memory and looping over them to see if one matches our criteria).
How is this working?
The Struct that the ApiJob extends has a :token attribute. This token is based on the attributes passed (attr1, attr2, attr3) and is built when a new class extending ApiJob is instantiated.
The find_by_token class method simply searches the string representation of the job in the delayed_job queue for a match based on a token built using the same token class method.
I have this method in my reports_controller.rb, which allows an user to send a status.
def send_status
date = Date.today
reports = current_user.reports.for_date(date)
ReportMailer.status_email(current_user, reports, date).deliver
head :ok
rescue => e
head :bad_request
end
How can I call this action from ActiveAdmin, in order to check if a User sent this report or not? I want it like a status_tag on a column or something.
Should I do a member action?
Thanks!
I'll address the issue of checking if a report has been sent later, but first I'll cover the question of how to call the controller action from ActiveAdmin.
While you can call ReportsController#send_status by creating an ActionController::Base::ReportsController and then calling the desired method, e.g.
ActionController::Base::ReportsController.new.send_status
this isn't a good idea. You probably should refactor this to address a couple potential issues.
app/controllers/reports_controller.rb:
class ReportsController < ApplicationController
... # rest of controller methods
def send_status
if current_user # or whatever your conditional is
ReportMailer.status_email(current_user).deliver
response = :ok
else
response = :bad_request
end
head response
end
end
app/models/user.rb:
class User < ActiveRecord::Base
... # rest of user model
def reports_for_date(date)
reports.for_date(date)
end
end
app/mailers/reports_mailer.rb
class ReportsMailer < ActionMailer::Base
... # rest of mailer
def status_email(user)
#user = user
#date = Date.today
#reports = #user.reports_for_date(#date)
... # rest of method
end
end
This could obviously be refactored further, but provides a decent starting point.
An important thing to consider is that this controller action is not sending the email asynchronously, so in the interest of concurrency and user experience, you should strongly consider using a queuing system. DelayedJob would be an easy implementation with the example I've provided (look into the DelayedJob RailsCast).
As far as checking if the report has been sent, you could implement an ActionMailer Observer and register that observer:
This requires that the User model have a BOOLEAN column status_sent and that users have unique email address.
lib/status_sent_mail_observer.rb:
class StatusSentMailObserver
self.delivered_email(message)
user = User.find_by_email(message.to)
user.update_attribute(:status_sent, true)
end
end
config/intializer/setup_mail.rb:
... # rest of initializer
Mail.register_observer(StatusSentMailObserver)
If you are using DelayedJob (or almost any other queuing system) you could implement a callback method to be called on job completion (i.e. sending the status email) that updates a column on the user.
If you want to track the status message for every day, you should consider creating a Status model that belongs to the User. The status model could be created every time the user sends the email, allowing you to check if the email has been sent simply by checking if a status record exists. This strategy is one I would seriously consider adopting over just a simple status_sent column.
tl;dr ActionController::Base::ReportsController.new.send_status & implement an observer that updates a column on the user that tracks the status. But you really don't want to do that. Look into refactoring like I've mentioned above.
I'm sending emails out through resque. All emails send properly except this one, which sends fine in development locally but fails on staging server.
It seems to view the 'Admin' object as a hash instead of treating it as an admin object. Any ideas?
account.rb
class Account < ActiveRecord::Base
after_commit :send_welcome_email
def send_welcome_email
SubscriptionNotifier.welcome(self).deliver
end
end
subscription_notifier.rb
class SubscriptionNotifier < ActionMailer::Base
def welcome(account)
#subscriber = account
mail(:to => account.admin.email, :subject => "Welcome!")
end
end
Resque error
SubscriptionNotifier Arguments
"welcome"
{"account"=>{"address_line1"=>nil, "address_line2"=>nil, "city"=>nil, "created_at"=>"2012-02-08T10:56:22-08:00", "currency"=>"United States Dollar (USD)", "deleted_at"=>nil, "description"=>nil, "email"=>"test2#test.com", "full_domain"=>"www.test.net", "id"=>3, "initial_plan"=>nil, "latitude"=>nil, "longitude"=>nil, "name"=>"macs", "phone"=>nil, "setup_steps_complete"=>0, "state"=>nil, "time_zone"=>"Pacific Time (US & Canada)", "updated_at"=>"2012-02-08T10:56:22-08:00", "website"=>nil, "zip"=>nil}}
Exception
NoMethodError
Error
undefined method `admin' for #<Hash:0x0000000585aa70>
I think you should just pass in the account ID into the queue and have the worker fetch the Account object when it does its perform method. That should lessen your Hash woes.
This is an old question, but still relevant. The answers here give workarounds, but don't describe why the problem arises or how to design jobs to avoid it.
Basically for Resque to persist jobs in Redis, the arguments need to be serialized so they can be saved. You can't save a Ruby object in a database (for example), so the arguments are serialized to JSON (which can be persisted). In your case, it's calling account.to_json and stores that as an argument to your job.
The likely reason this is an issue in staging but not in development, is your development Redis is only storing the jobs in memory (and therefore they don't need to be serialized). Staging is persisting them to disk or a database for example, so they have to be serialized.
To avoid this problem, arguments to jobs should be strings, numbers, or data structures that can be converted to json.
You will need to load the environment by running 'rake environment resque:work QUEUE='*' RAILS_ENV=staging'
that is unless you have
task "resque:setup" => :environment
defined in a resque.rake file.
Try this:
in account.rb
def send_welcome_email
SubscriptionNotifier.welcome(self.admin.email).deliver
end
in subscription_notifier.rb
def welcome(account_admin_email)
#subscriber = account
mail(:to => account_admin_email, :subject => "Welcome!")
end
I had the same error, apparently you are passing account as a hash and it has no email admin method. So just get the email in the send_welcome_email method and pass it as a parameter, instead to passing a hash and trying to access the email in the welcome method.
NOTE: for the #subscriber, you would need to pass the parameters, just like the email, you use in the email template for example self.admin.name in the model and #name = account_admin_name in the welcome method
Hope this helps.