Altering fields included in Rails' default JSON/XML serialization - ruby-on-rails-3

I'm writing a web service to return a user's details. In the controller, I simply render :xml => user and return. However, not all of the fields of my User model are being returned, and I don't see anything in my model that would indicate which fields to include or exclude.
Model:
class User < ActiveRecord::Base
devise :database_authenticatable,
:recoverable, :rememberable, :trackable, :validatable
attr_accessible :email, :password, :password_confirmation, :remember_me, :first_name, :last_name
end
Example:
irb(main):003:0> #user = User.find(3)
=> #<User id: 3, email: "me#me.me", encrypted_password: <redacted>, reset_password_token: nil, reset_password_sent_at: nil, remember_created_at: nil, sign_in_count: 11, current_sign_in_at: "2011-08-24 22:50:44", last_sign_in_at: "2011-08-24 06:18:41", current_sign_in_ip: "1.2.3.4", last_sign_in_ip: "1.2.3.4", created_at: "2011-08-23 17:09:28", updated_at: "2011-08-26 04:01:01", controller: false, admin: false, chargify_customer_id: 1234, chargify_subscription_id: 1234, first_name: "Me", last_name: "Me", chargify_subscription_state: "active">
What my render is currently returning for that same user:
<?xml version="1.0" encoding="UTF-8"?>
<user>
<last-name>Me</last-name>
<email>me#me.me</email>
<first-name>Me</first-name>
</user>
At a minimum, I need to include the id field; overall, I'd like to understand better how you control what gets included and what doesn't.

Serialization happens in the as_json/as_xml methods. By default these methods serialize all of your models attributes into json/xml. However, devise hides certain attributes generated by its ActiveRecord extensions. That's why you don't get the password fields for example.
You can control which attributes get included in your xml by overriding the to_xml method in your user model.
def as_xml(options = {})
default_options = {
:only => [:id, :first_name, :last_name, :email]
}
xml_options = options.blank? ? default_options : options
super xml_options
end
You can also include custom methods of your model.
def as_xml(options = {})
default_options = {
:only => [:id, :first_name, :last_name, :email],
:methods => [:some_custom_method]
}
xml_options = options.blank? ? default_options : options
super xml_options
end
You can read more about serialization here:
http://api.rubyonrails.org/classes/ActiveRecord/Serialization.html

Related

Nested `accepts_nested_attributes_for` hitting `undefined method 'with_indifferent_access'`

I'm writing specs for creating a new account. Here's the situation:
Ruby 2.5.0, Rails 5.2.0.rc1
class Account
has_many :accounts_users, inverse_of: :account
has_many :users, through: :accounts_users
has_one :owner, -> { AccountsUser.admins.order(:id) }, class_name: 'AccountsUser'
accepts_nested_attributes_for :owner
end
class AccountsUser
belongs_to :account, inverse_of: :accounts_user
belongs_to :user, autosave: true, inverse_of: :accounts_user
accepts_nested_attributes_for :user
end
class User
has_many :accounts, through: :accounts_users
has_many :accounts_users, autosave: true, dependent: :destroy, inverse_of: :user
accepts_nested_attributes_for :accounts_users, reject_if: :all_blank
end
# AccountsController#account_create_params
params.require(:account).permit(
:plan_id,
:name,
:subdomain,
{
owner_attributes: {
user_attributes: [
:email,
:first_name,
:last_name
]
}
}
)
# rspec account_create_params
let(:valid_account_params) do
{
plan_id: plan.id,
name: 'Name',
subdomain: 'subdomain',
owner_attributes: {
user_attributes: [
email: 'jane#doe.com',
first_name: 'Jane',
last_name: 'Doe'
]
}
}
end
#account = Account.new(account_create_params)
#> NoMethodError: undefined method `with_indifferent_access' for #<Array:0x000056472e0799c0>
from /usr/local/bundle/gems/activerecord-5.2.0.rc1/lib/active_record/nested_attributes.rb:412:in `assign_nested_attributes_for_one_to_one_association'
It has to be complaining about the user_attributes array, and seeing as it's coming from a method called assign_nested_attributes_for_one_to_one_association, Rails seems to be erroneously thinking that the relationship between AccountsUsers and Users is one to one and not one to many.
I've triple checked the associations. What do you guys think? Rails 5.2 bug perhaps?
There were several issues here. I found that permitting parameters and providing them needed different syntax:
def account_create_params
params.require(:account).permit(
:plan_id,
:name,
:subdomain,
{
owner_attributes: {
user_attributes: [
:email,
:first_name,
:last_name,
:password
]
}
}
)
end
vs
let(:valid_account_params) do
{
plan_id: plan.id,
name: 'Name',
subdomain: 'subdomain',
owner_attributes: {
user_attributes: {
email: email_valid,
first_name: 'Jane',
last_name: 'Doe',
password: 'password'
}
}
}
end
A bit confusing there but it works. The inverse_of on AccountsUser was wrong. I needed plural:
belongs_to :account, inverse_of: :accounts_users, optional: true
belongs_to :user, autosave: true, inverse_of: :accounts_users
And I also needed optional: true on the account belongs to, despite accepts_nested_attributes_for setting up the relationship properly. There was a chicken vs egg situation where the AccountsUser was invalid because the Account didn't exist, and the Account couldn't be saved because the AccountsUser was invalid. This may be a Rails 5 'feature'.

Rails 4 creating relational record

I am having some issues creating a record from a relational record. This code creates a new user perfectly however it seems that it skips over creating the user's profile all together. It also throws no errors.
any help would be great.
Model
class User < ActiveRecord::Base
has_one :profile
def self.find_for_facebook_oauth(auth)
if user = User.find_by_email(auth.info.email)
user
else
user = User.create( provider: auth.provider,
uid: auth.uid,
email: auth.info.email,
password: Devise.friendly_token[0,20] )
user.build_profile( username: auth.extra.raw_info.username,
first_name: auth.info.first_name,
last_name: auth.info.last_name,
gender: auth.extra.raw_info.gender,
country: auth.extra.raw_info.locale,
image: auth.info.image )
user
end
end
end
Try user.create_profile instead.http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html
I forgot that in my Profile class I had a validation for username
model
class Profile < ActiveRecord::Base
# Use friendly_id
extend FriendlyId
friendly_id :username, use: :slugged
belongs_to :users
mount_uploader :image, ProfileImageUploader
validates :username, presence: true, length: {maximum: 16} # <~~
validates :first_name, presence: true, length: {maximum: 255}
validates :last_name, presence: true, length: {maximum: 255}
validates :gender, presence: true, inclusion: %w(male female)
end
My OmniAuth was not giving the username therefore it was rolling back the commit.

Defining FactoryGirl for User model of Devise fails

I've spent the last two hours figuring out what is wrong, but could not find the answer anywhere.
Its my first rails application (except Hartl's tutorial) so the solution might be simple.. I'm using Devise to manage my Users, everything is really ok with it up until now.
Trying to test the User model I defined a factory like this:
FactoryGirl.define do
factory :user do
email "g#g.com"
password "123123"
password_confirmation { "123123" }
end
end
and the test is:
describe User do
# pending "add some examples to (or delete) #{__FILE__}"
#user = FactoryGirl.create(:user)
subject(:user)
it { should respond_to(:email) }
it { should respond_to(:password) }
it { should be_valid }
end
But the last line ( it { should be_valid } ) fails the test.
I've printed the value of user/#user (tried both) and it came out nil.
Edit: It's not nil. Its
#<User id: 13, email: "email1#factory.com", encrypted_password: "$2a$04$.lWs6yadJu/Ya67xi.W1F.fd6sWLGkzc/59.lgTi0sA7...", reset_password_token: nil, reset_password_sent_at: nil, remember_created_at: nil, sign_in_count: 0, current_sign_in_at: nil, last_sign_in_at: nil, current_sign_in_ip: nil, last_sign_in_ip: nil, created_at: "2012-08-27 15:48:23", updated_at: "2012-08-27 15:48:23">
class User < ActiveRecord::Base
# Include default devise modules. Others available are:
# :token_authenticatable, :confirmable,
# :lockable, :timeoutable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable
# Setup accessible (or protected) attributes for your model
attr_accessible :email, :password, :password_confirmation, :remember_me
# attr_accessible :title, :body
validates :email, :presence => true
validates :password, :presence => true
end
What is it that I don't see?
OK, I finally found the problem. It turns out you have to restart "spork" when ever you make a change to the User model, because it preloads it.

confused about how to use bcrypt-ruby

I am implementing a validation scheme and am using the bcrypt-ruby gem.
require 'bcrypt'
class User < ActiveRecord::Base
include BCrypt
attr_accessor :password
attr_accessible :name, :email, :password, :password_confirmation
validates :password, :presence => true, :on => :create,
:confirmation => true,
:length => {:within => 6..12}
before_save :encrypt_password
def has_password?(submitted_password)
self.encrypted_password == submitted_password # this calls a method in bcrypt
# File lib/bcrypt.rb, line 171
# def ==(secret)
# super(BCrypt::Engine.hash_secret(secret, #salt))
# end
end
private
def encrypt_password
self.encrypted_password = Password.create(password, :cost => 5)
end
end
Now in the console I create a new user
>> user = User.create!(:name => "test", :email => "test#test.com", :password => "foobar", :password_confirmation => "foobar")
=> #<User id: 1, name: "test", email: "test#test.com", created_at: "2011-06-23 05:00:00", updated_at: "2011-06-23 05:00:00", encrypted_password: "$2a$10$I7Wy8NDMeVcNgOsE3J/ZyubiNAESyxA7Z49H4p1x5xxH...">
And if I check if the password is valid I do the following:
>> user.has_password?("foobar")
=> true
but if I get the user from the database it fails:
user = User.find(1)
user.has_password?("foobar")
=> false
Why does that happen and how can I implement bcrypt to make this work?
Thank you in advance.
My guess would be that since encrypted_password is stored in the database as a string and not a BCrypt::Password, you're not calling into BCrypt's ==, but rather String's ==. You have to instantiate an instance of the Password around the string hash value. That would be where I'd look.
As described over here you have to use the password class of Bcrypt to utilize the ==
def has_password?(submitted_password)
Bcrypt::Password.new(self.encrypted_password) == submitted_password
end

Rails habtm grief am I dumb or rails bug? (desperate ; )

Greetings all,
I'm having a weird problem with a habtm relationship and honestly I'm beginning to think I may have stumbled upon some weird bug in rails 3. Surely I'm crazy though. I've been beating my head against the wall on this for 3 days, have googled everything under the sun I can think of and still can't come up with an answer.
Ok, the situation:
I'm creating a Rails app to replace both a Java app and a PHP app (java application and php front-end). This is going to be a phased operation with the first phase being the Rails application takes over registration and billing. In order to do this, the Rails application must create data in the databases for the Java and PHP apps. The Rails application itself is using Devise for authentication.
In database.yml I have my standard 3 databases defined and also a connection defined for the Java apps database.
Here are pieces of the model definitions for the external object (I'm just creating regular rails models to talk to the external databases):
class Pushbroom::UserAccount < ActiveRecord::Base
require 'digest/md5'
require 'base64'
establish_connection :pushbroom
set_table_name :user_account
set_primary_key :id
has_and_belongs_to_many :user_roles, :join_table => 'pb_prod.users_roles', :class_name => 'Pushbroom::UserRole', :foreign_key => 'user_account_id', :association_foreign_key => 'user_role_id'
belongs_to :user, :dependent => :destroy
attr_accessible :user_roles, :admin_notes, :enabled, :username, :password_hash, :prefStore, :accepted_tos, :do_not_contact
end
class Pushbroom::UserRole < ActiveRecord::Base
establish_connection :pushbroom
set_table_name :user_role
set_primary_key :id
has_and_belongs_to_many :user_accounts, :join_table => 'pb_prod.users_roles', :class_name => 'Pushbroom::UserAccount', :foreign_key => 'user_role_id', :association_foreign_key => 'user_account_id'
end
And finally my Rails application user object:
class User < ActiveRecord::Base
after_create :send_welcome_email
before_save :create_pushbroom_user_data
# Include default devise modules. Others available are:
# :token_authenticatable, :encryptable, :confirmable, :lockable, :timeoutable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable
belongs_to :pb_user_account, :class_name => "Pushbroom::UserAccount", :foreign_key => "pb_user_account_id", :dependent => :destroy, :autosave => true
# Setup accessible (or protected) attributes for your model
attr_accessible :first_name, :last_name, :username, :dob, :email, :password, :password_confirmation, :remember_me
validates_presence_of :first_name, :last_name, :username, :dob
validates_date :dob, :on_or_after => lambda { 100.years.ago }, :on_or_after_message => "must be on or after #{100.years.ago.strftime('%m-%d-%Y')}"
validates_date :dob, :on_or_before => lambda { 13.years.ago }, :on_or_before_message => "must be on or before #{13.years.ago.strftime('%m-%d-%Y')}"
def create_pushbroom_user_data
pb_user = create_pushbroom_user
pb_user_account = create_pushbroom_user_account(pb_user)
pb_user_account.user_roles << Pushbroom::UserRole.find_by_name('user')
self.pb_user_account = pb_user_account
end
def create_pushbroom_user
pb_user = Pushbroom::User.new
pb_user.attributes = self.attributes.slice(
"email",
"first_name",
"last_name",
"dob")
pb_user
end
def create_pushbroom_user_account(pb_user)
pb_user_account = Pushbroom::UserAccount.new
pb_user_account.enabled = true
pb_user_account.password_hash = Pushbroom::UserAccount.create_password_digest(#plaintext_password, self.username)
pb_user_account.username = self.username
pb_user_account.user = pb_user
pb_user_account
end
Seems like it should be pretty vanilla. The ONLY weirdness here is that they aren't in the native rails database and one of the fields is named funny in the relations table.
So here's a rails console session where I create a rails user, call the method to create the external objects, then try to save:
ruby-1.9.2-p180 :001 > def user_fred
ruby-1.9.2-p180 :002?> {
ruby-1.9.2-p180 :003 > :first_name => "Fred",
ruby-1.9.2-p180 :004 > :last_name => "Flinstone",
ruby-1.9.2-p180 :005 > :username => "fflint",
ruby-1.9.2-p180 :006 > :dob => "1986-06-01",
ruby-1.9.2-p180 :007 > :email => "fred#mydomain.org",
ruby-1.9.2-p180 :008 > :password => "badpass"
ruby-1.9.2-p180 :009?> }
ruby-1.9.2-p180 :010?> end
=> nil
ruby-1.9.2-p180 :011 > user = User.new(user_fred)
=> #<User id: nil, email: "fred#mydomain.org", encrypted_password: "$2a$10$IiEOEoSnXIrP7VJAQYckfOVXuzm7Y5ZGo20ayLpSkHhz...", reset_password_token: nil, remember_created_at: nil, sign_in_count: 0, current_sign_in_at: nil, last_sign_in_at: nil, current_sign_in_ip: nil, last_sign_in_ip: nil, created_at: nil, updated_at: nil, first_name: "Fred", last_name: "Flinstone", username: "fflint", dob: "1986-06-01", pb_user_account_id: nil>
ruby-1.9.2-p180 :012 > user.create_pushbroom_user_data
=> #<Pushbroom::UserAccount id: nil, created_by: nil, created_at: nil, updated_by: nil, updated_at: nil, admin_notes: nil, enabled: true, username: "fflint", password_hash: "blah blah", user_id: nil, prefStore: nil, accepted_tos: nil, do_not_contact: nil>
ruby-1.9.2-p180 :013 > user.pb_user_account.user_roles
=> [#<Pushbroom::UserRole id: 1, created_by: "script", created_at: "2008-11-10 12:10:44", updated_by: "script", updated_at: "2008-11-10 12:10:44", admin_notes: "", name: "user", description: "Generic User Role", conditional: false>]
ruby-1.9.2-p180 :014 > user.save!
NoMethodError: undefined method `relation' for nil:NilClass
from /Users/gander/.rvm/gems/ruby-1.9.2-p180#sms2/gems/activesupport- 3.0.5/lib/active_support/whiny_nil.rb:48:in `method_missing'
from /Users/gander/.rvm/gems/ruby-1.9.2-p180#sms2/gems/arel- 2.0.9/lib/arel/insert_manager.rb:22:in `insert'
from /Users/gander/.rvm/gems/ruby-1.9.2-p180#sms2/gems/arel- 2.0.9/lib/arel/crud.rb:26:in `insert'
from /Users/gander/.rvm/gems/ruby-1.9.2-p180#sms2/gems/activerecord- 3.0.5/lib/active_record/associations/has_and_belongs_to_many_association.rb:76:in `insert_record'
from /Users/gander/.rvm/gems/ruby-1.9.2-p180#sms2/gems/activerecord- 3.0.5/lib/active_record/associations/association_proxy.rb:151:in `send'
from /Users/gander/.rvm/gems/ruby-1.9.2-p180#sms2/gems/activerecord-3.0.5/lib/active_record/autosave_association.rb:306:in `block in save_collection_association'
from /Users/gander/.rvm/gems/ruby-1.9.2-p180#sms2/gems/activerecord-3.0.5/lib/active_record/associations/association_collection.rb:431:in `block in method_missing'
from /Users/gander/.rvm/gems/ruby-1.9.2-p180#sms2/gems/activerecord-3.0.5/lib/active_record/associations/association_proxy.rb:216:in `block in method_missing'
from /Users/gander/.rvm/gems/ruby-1.9.2-p180#sms2/gems/activerecord-3.0.5/lib/active_record/associations/association_proxy.rb:216:in `each'
from /Users/gander/.rvm/gems/ruby-1.9.2-p180#sms2/gems/activerecord-3.0.5/lib/active_record/associations/association_proxy.rb:216:in `method_missing'
from /Users/gander/.rvm/gems/ruby-1.9.2-p180#sms2/gems/activerecord-3.0.5/lib/active_record/associations/association_collection.rb:431:in `method_missing'
from /Users/gander/.rvm/gems/ruby-1.9.2-p180#sms2/gems/activerecord-3.0.5/lib/active_record/autosave_association.rb:297:in `save_collection_association'
from /Users/gander/.rvm/gems/ruby-1.9.2-p180#sms2/gems/activerecord-3.0.5/lib/active_record/autosave_association.rb:163:in `block in add_autosave_association_callbacks'
from /Users/gander/.rvm/gems/ruby-1.9.2-p180#sms2/gems/activesupport-3.0.5/lib/active_support/callbacks.rb:415:in `_run_create_callbacks'
from /Users/gander/.rvm/gems/ruby-1.9.2-p180#sms2/gems/activerecord-3.0.5/lib/active_record/callbacks.rb:281:in `create'
from /Users/gander/.rvm/gems/ruby-1.9.2-p180#sms2/gems/activerecord-3.0.5/lib/active_record/persistence.rb:246:in `create_or_update'
... 18 levels...
from /Users/gander/.rvm/gems/ruby-1.9.2-p180#sms2/gems/activerecord-3.0.5/lib/active_record/callbacks.rb:277:in `create_or_update'
from /Users/gander/.rvm/gems/ruby-1.9.2-p180#sms2/gems/activerecord-3.0.5/lib/active_record/persistence.rb:56:in `save!'
from /Users/gander/.rvm/gems/ruby-1.9.2-p180#sms2/gems/activerecord-3.0.5/lib/active_record/validations.rb:49:in `save!'
from /Users/gander/.rvm/gems/ruby-1.9.2-p180#sms2/gems/activerecord-3.0.5/lib/active_record/attribute_methods/dirty.rb:30:in `save!'
from /Users/gander/.rvm/gems/ruby-1.9.2-p180#sms2/gems/activerecord-3.0.5/lib/active_record/transactions.rb:245:in `block in save!'
from /Users/gander/.rvm/gems/ruby-1.9.2-p180#sms2/gems/activerecord- 3.0.5/lib/active_record/transactions.rb:292:in `block in with_transaction_returning_status'
from /Users/gander/.rvm/gems/ruby-1.9.2-p180#sms2/gems/activerecord-3.0.5/lib/active_record/connection_adapters/abstract/database_statements.rb:139:in `transaction'
from /Users/gander/.rvm/gems/ruby-1.9.2-p180#sms2/gems/activerecord-3.0.5/lib/active_record/transactions.rb:207:in `transaction'
from /Users/gander/.rvm/gems/ruby-1.9.2-p180#sms2/gems/activerecord-3.0.5/lib/active_record/transactions.rb:290:in `with_transaction_returning_status'
from /Users/gander/.rvm/gems/ruby-1.9.2-p180#sms2/gems/activerecord-3.0.5/lib/active_record/transactions.rb:245:in `save!'
from (irb):14
from /Users/gander/.rvm/gems/ruby-1.9.2-p180#sms2/gems/railties-3.0.5/lib/rails/commands/console.rb:44:in `start'
from /Users/gander/.rvm/gems/ruby-1.9.2-p180#sms2/gems/railties-3.0.5/lib/rails/commands/console.rb:8:in `start'
from /Users/gander/.rvm/gems/ruby-1.9.2-p180#sms2/gems/railties-3.0.5/lib/rails/commands.rb:23:in `<top (required)>'
from script/rails:6:in `require'
from script/rails:6:in `<main>'ruby-1.9.2-p180 :015 >
If I remove the role assignment, everything is just peachy (finds, saves, destroys, etc), but the second I try to save roles everything blows sky-high with this message that, frankly, I don't get. It knows its got the roles, there is no nil object that I can tell. . .and basically if I wasn't already bald I'd be pulling my hair out ; )
Any insight into this is EXTREMELY appreciated!
Gerald
P.S. Also asked here http://railsforum.com/viewtopic.php?id=43647 Will duplicate answer if found.
After beating myself silly for 4 days on this I finally found the problem: Rails (habtm) doesn't have the ability to determine the database to use for external relation tables. I also found the answer, and it doesn't even smell bad! There's a whole thread on the process here: http://groups.google.com/group/rubyonrails-talk/browse_thread/thread/c5655d0442039ccd
The answer? has_many :through - something I'd never taken much of a look at, but it is actually a pretty nice feature (even in other circumstances).
Basically this just allows me to create a model class which represents the relationship. And since I have a model class for it I can explicitly specify the database to connect to.
For posterity sake, here's the code:
class Pushbroom::UsersRolesRelationship < ActiveRecord::Base
establish_connection :pushbroom
set_table_name :users_roles
belongs_to :user_account
belongs_to :user_role
end
class Pushbroom::UserAccount < ActiveRecord::Base
establish_connection :pushbroom
set_table_name :user_account
set_primary_key :id
has_many :users_roles_relationships
has_many :user_roles, :through => :users_roles_relationships, :source => :user_role
end
class Pushbroom::UserRole < ActiveRecord::Base
establish_connection :pushbroom
set_table_name :user_role
set_primary_key :id
has_many :users_roles_relationships
has_many :user_accounts, :through => :users_roles_relationships, :source => :user_account
end
And is used thusly:
def add_subscription_plan_roles_to_pb_user_account(pb_user_account)
roles_granted = pb_user_account.user.subscriptions.first.subscription_plan.roles_granted
pb_user_account.user_roles = roles_granted
end
Thanks a ton folks for helping me get this train moving again! All my tests are passing and it seems to be working, but if you see something wrong, please do still let me know.
Thanks!
Gerald
Try doing some manual saves of the objects created in various places, such as the create_pushbroom_user_account(pb_user) method. I have had some issues in the past when relying on the "autosave" system.