I'm using delayed job 3.0.2 with ActiveRecord and Rails 3.2.3. I have a User model which uses the has_secure_password mixin, so the password is only stored encrypted. Now I want to use delayed job to send the welcome email, which should contain a copy of the unencrypted password.
When creating the record, the plain-text password is in User#password. But delayed job seems to serialize/ deserialize the id of the record only and create a new instance of the model by doing User.find(X). This way my plain-text password is lost and the user gets an empty password in his email.
How can I tell delayed-job to serialize/ deserialize custom "virtual" attributes too, which are not stored in the database otherwise?
This is my monkey patch for delayed job 2.x, which worked fine.
class ActiveRecord::Base
def self.yaml_new(klass, tag, val)
klass.find(val['attributes']['id']).tap do |m|
val.except("attributes").each_pair{ |k, v| m.send("#{k}=", v) }
end
rescue ActiveRecord::RecordNotFound
raise Delayed::DeserializationError
end
end
It doesn't work with delayed job 3.x. I'm also not really interested in fixing my monkey patch as I hope there's a proper solution to this.
In delayed job 3.x, the best way to do this is to override a few methods on your ActiveRecord class, and then to force the Psych YAML deserializer to load the ActiveRecord object from the serialized data. By default, delayed job uses just the deserialized id, and then loads the ActiveRecord object from the DB. So, say I have an ActiveRecord class called ShipmentImport, and I want an attr_accessor named 'user_id' to work with delayed job serialization/deserialization. Here is what I would do.
In the ShipmentImport ActiveRecord class, add this:
def encode_with(coder)
super
coder['user_id'] = #user_id
end
def init_with(coder)
super
#user_id = coder['user_id']
self
end
In an initializer for your application, add this for your ActiveRecord class:
Psych.load_tags[['!ruby/ActiveRecord', ShipmentImport.name].join(':')] = ShipmentImport
Related
In Rails, I have created a Model that retrieves users from an LDAP database rather than from ActiveRecord. Now I am attempting to integrate my ActiveRecord models with the LDAP-based models, so I am writing methods in my models that emulate some common ActiveRecord methods.
One of the methods I am trying to emulate is one that is normally created by the has_many through relationship on ActiveRecord. In ActiveRecord, this relationship would allow the following:
user = User.first
groups = user.groups # == Array of Groups
groups << Group.create(name: "Test") # How does Rails allow this?
How exactly does Rails allow this? I've tried dynamically assigning methods to the array instance returned by user.groups, but there doesn't seem to be any way to make those methods aware of which user record the array was created from. (So they can assign user_id on the new relationship record.) What am I missing?
Though user.groups appears to be an array of groups, it's actually an entirely separate class -- a Rails internal class that you usually don't know much about called an association proxy. The proxy responds to methods like <<, create, new and so on by proxying requests to the target class and then setting the association appropriately.
If you want similar functionality you'll have to implement your own kind of proxy associations. Doing so will be pretty complicated, but this might get you started.
module LDAP
class Association
attr_accessor :source, :target
def initialize(source, target)
#source = source
#target = target
end
def <<(obj)
#source.group_ids = [group_ids + obj].flatten.uniq
#source.save
end
end
end
class User
def groups
LDAP::Association.new(self, Group)
end
end
This is not even particularly close to how ActiveRecord implements association proxies. However, this is quite a bit simpler than ActiveRecord's solution and should be enough to duplicate some basic ActiveRecord functionality.
I would go about doing this by peeking into the Rails Source Code, e.g. the code for the
Group.create example above can be found in
http://api.rubyonrails.org/classes/ActiveRecord/Persistence/ClassMethods.html
def create(attributes = nil, options = {}, &block)
if attributes.is_a?(Array)
attributes.collect { |attr| create(attr, options, &block) }
else
object = new(attributes, options, &block)
object.save
object
end
end
end
I am playing around with Devise in a project, and am just trying to better understand how it all works. The Sessions controller in particular is doing a few things that I don't understand:
class Devise::SessionsController < ApplicationController
def new
# What benefit is this providing over just "resource_class.new"?
self.resource = resource_class.new(sign_in_params)
clean_up_passwords(resource)
# What is "serialize_options" doing in the responder?
respond_with(resource, serialize_options(resource))
end
def create
self.resource = warden.authenticate!(auth_options)
set_flash_message(:notice, :signed_in) if is_navigational_format?
sign_in(resource_name, resource)
respond_with resource, :location => after_sign_in_path_for(resource)
end
...
protected
...
def serialize_options(resource)
methods = resource_class.authentication_keys.dup
methods = methods.keys if methods.is_a?(Hash)
methods << :password if resource.respond_to?(:password)
{ :methods => methods, :only => [:password] }
end
def sign_in_params
devise_parameter_sanitizer.sanitize(:sign_in)
end
end
I assume that these methods are adding some sort of security. I'd just like to know what exactly they are protecting against.
The implementation of devise_parameter_sanitizer is creating a ParameterSanitizer instance. I often find looking at tests to be helpful in understanding the purpose of code; and this test I think illustrates it best-- since you're creating a new user, you don't want to allow users to assign any value they want to any parameter they want, so sanitize means "strip out any attributes other than the ones we need for this action". If this wasn't here, and you had a role attribute, a user could send a specially-crafted POST request to make themselves an admin when signing up for your site. So this protects against what's called, in the general case, a mass assignment vulnerability. Rails 3 and Rails 4 protect against this in different ways but the protections can still be turned off, and I'm guessing Devise is trying to set some good-practice defaults.
The serialize_options method is creating a hash of options to support rendering to XML or JSON. I found this out by looking at the implementation of responds_with, which calls extract_options! which uses the last argument as options if the last argument is a Hash. The documentation for responds_with says "All options given to #respond_with are sent to the underlying responder", so I looked at ActionController::Responder, whose documentation explains the process it takes to look for a template that matches the format, then if that isn't found, calling to_#{format}, then calling to_format. There's a view for HTML, and ActiveRecord objects respond to to_xml and to_json. Those methods use those options:
The <tt>:only</tt> and <tt>:except</tt> options can be used to limit the
attributes included, and work similar to the +attributes+ method.
To include the result of some method calls on the model use <tt>:methods</tt>.
So this keeps you from exposing more information than you might want to if someone uses the XML or JSON format.
I have this problem related to testing model errors with Mocha:
This is my controller (Api / Artist controller):
class Api::ArtistsController < ApplicationController
respond_to :json
def create
artist = Artist.new(params[:artist])
if artist.save <-- This is where the test fails
render :json=>artist
else
respond_with artist
end
end
end
This is my model (Artist Model):
class Artist < ActiveRecord::Base
include Core::BaseModel
attr_accessible :name
has_many :albums
validates :name, :presence=>true, :uniqueness=>{:case_sensitive=> false}
default_scope where :deleted=>false
end
This is the test where it fails, about Artist controller:
it "should not save a duplicated artist" do
Artist.any_instance.stubs(:is_valid?).returns(false)
Artist.any_instance.stubs(:errors).returns({:name=>[I18n.t('activerecord.errors.messages.taken')]})
post :create, :format => :json
expect(response).not_to be_success
expect(response.code).to eq("422")
results = JSON.parse(response.body)
expect(results).to include({
"errors"=>{
"name"=>[I18n.t('activerecord.errors.messages.taken')]
}
})
end
When I run the tests, this is the error I get on the above test:
Failure/Error: post :create, :format => :json
NoMethodError:
undefined method `add_on_blank' for {}:Hash
# ./app/controllers/api/artists_controller.rb:17:in `create'
# ./spec/controllers/api/artists_controller_spec.rb:56:in `block (3 levels) in <top (required)>'
I'm starting to use Mocha, so I don't know if there's a way to test the json result for the specific case when I want to test the validation for the duplicated name.
ActiveRecord::Base#errors (i.e. Artist#errors) isn't a simple hash. It's supposed to be an instance of ActiveModel::Errors. You're stubbing it with a hash, and ActiveRecord is trying to call add_on_blank on it, which is failing.
I don't think save invokes is_valid? at all, and I suspect it's running the validations and then trying to call add_on_blank to append an error, but since you've stubbed out errors, that's failing.
This isn't really a good way to test the controller. It's making too many assumptions about the internals of Artist. You're also testing things that aren't part of the controller at all; errors isn't referenced anywhere in the action. The only behavior worth testing in the controller is whether or not it creates an Artist; if that Artist fails to save, that it renders JSON with it; and if the save succeeds, that it redirects. That's all of the controller's responsibility.
If you want to test that errors are rendered a certain way, you should write a separate view spec. If you want to test that missing fields generate errors, you should write a model spec. If you don't want to write a view spec, it's still sufficient to rely on the model to populate errors (tested in a model spec), and in your controller, just test that render is called with json set to the Artist instance.
Generally speaking it's best to avoid stubbing as much as possible, but in this case, the only things I'd consider stubbing are Artist.new to return a mock, and save on that mock to return false. Then I'd check to make sure it rendered with the mock.
The easier option is to just create an actual Artist record, then call post with duplicate params to trigger a validation failure. The downside is that you hit the database, and avoiding that in a controller spec is laudable, but generally more convenient. You could instead do that in a Capybara feature spec if you want to avoid DB hits in your controller specs.
If you want to try testing the way you are, you can manually create an instance of ActiveModel::Errors and populate that, or stub methods on it, and stub out Artist.any_instance.stubs(:errors) to return your mock with ActiveModel::Errors-compatible behavior, but that's a lot of mocking.
One final tip: don't use post :create, :format => :json. Use xhr :post, :create to generate a real Ajax request rather than relying on a format param. It's a more robust test of your routing and response code.
Whenever a User object is created, I want a UserInfo object to be created too, and linked to it.
Unfortunately this does not create any UserInfo:
class User < ActiveRecord::Base
has_one :user_info
...
def init
self.user_info = user_info
self.save!
end
Why is the init method not called? How to reach my goal?
sombe's technique is right, but his details aren't ideal. In fact, since create_user_info is already a method on User instances, all you want is something like:
class User < ActiveRecord::Base
has_one :user_info
before_create :create_user_info
end
Edit: init doesn't do anything particularly magical under Rails (I... don't think it does under basic Ruby either - are you thinking of initialize? I'll assume you are). initialize is fired off when an instance of the Ruby class is created in memory. That's divorced by quite some margin from an instance of the model being created in the database; a new class instance could be due to you calling build (and not saving yet), or even due to reading an instance out of the database.
If you want to step in on database operations, you need to make use of the ActiveRecord callbacks. You might find my answer to this question useful.
before_save callback triggers on create and update.
I'd suggest to use after_create because before_create can return errors
class User < ActiveRecord::Base
has_one :user_info
...
after_create do
create_user_info
end
In your User model, use a before_save filter instead of init like this:
before_save :create_user_info
...
private
def create_user_info
user_info = UserInfo.new
if user_info.save
self.user_info_id = user_info.id
end
end
I have a Post, to which a migration adds a new attribute and table column short_url. This attribute is either provided by the user, or, if left blank, automatically created:
class Post < ActiveRecord::Base
before_create :create_short_url
private
def create_short_url
if short_url.blank? || already_exists?(short_url)
write_attribute :short_url, random_string(6)
end
end
def random_string(length)
#innards are irrelevant for this question
end
end
In the migration, I want to run through all posts and have the short_url created and saved.
problem: Post.find(:all).each {|post| post.create_short_url} in the self.up is not possible, due to the private scope of the create_short_url method.
problem: Looping through posts and update!-ing them does not invoke the before_create :create_short_url, because it is not before create. Once migrated, I prefer to not have any before_update hooks in place: I don't need to change anything on update.
How would you tackle this? Copy over the random_string() and associated methods to the migration? Add specific migration helper methods to the Post?
Just use the Object method send (it doesn't check protected/private).
Post.all.each do |post|
post.send :create_short_url
post.save!
end
An alternative would be (but that could interfere with other migrations running in the same Ruby-process after that):
Post.before_save :create_short_url
Post.all.each(&:save!)
Visibility tip: Most of the time what you really mean is protected (see here). I recommend to use protected instead of private in this case.