tl,dr: How can I ensure that the order of my has_many, through nested attributes, with an attribute value set in the build, are always assigned the same nested parameters hash key number (0, 1, etc.) and always appear in the form in the same order?
Hopefully I can describe this so it makes sense. I have a small prototype app that simulates a simple bank transfer between two accounts, a source account and a destination account. I have a Transfer class, an Account class and a TransferAccounts class that is the through join for the many_to_many association between Transfer and Account.
Here is the new action in the TransfersController:
def new
#transfer = Transfer.new
#transfer.transfer_accounts.build(account_transfer_role: 'source').build_account
#transfer.transfer_accounts.build(account_transfer_role: 'destination').build_account
bank_selections
account_selections
end
And the strong parameters:
def transfer_params
params.require(:transfer).
permit(:name, :description,
transfer_accounts_attributes:
[:id, :account_id, :account_transfer_role,
account_attributes:
[:id, :bank_id, :name, :description, :user_name,
:password, :routing_number, :account_number
]
])
end
So, the two transfer_accounts associated with a transfer each have an account_transfer_role attribute, with one of them set to source and the other set to destination.
Now, when filling in the form and submitting, the parameters look like the following in console:
Parameters: {"utf8"=>"✓", "authenticity_token"=>"xxxxxxxxxxxxxxxxxx==",
"transfer"=>{"name"=>"Test Transfer 2", "description"=>"Second test transfer",
"transfer_accounts_attributes"=>{"0"=>{"account_transfer_role"=>"source", "account_id"=>"1",
"account_attributes"=>{"bank_id"=>"1", "name"=>"George's Checking",
"description"=>"George's personal checking account", "user_name"=>"georgeckg",
"password"=>"[FILTERED]", "account_number"=>"111111111", "routing_number"=>"101010101",
"id"=>"3"}, "id"=>"3"}, "1"=>{"account_transfer_role"=>"destination", "account_id"=>"2",
"account_attributes"=>{"bank_id"=>"2", "name"=>"George's Savings",
"description"=>"George's personal savings account", "user_name"=>"georgesav",
"password"=>"[FILTERED]", "account_number"=>"111101111", "routing_number"=>"100100100",
"id"=>"4"}, "id"=>"4"}}}, "commit"=>"Update Transfer", "id"=>"2"}
As you can see, each transfer_account in the the transfer_account_attributes hash has an id key, either a 0 or a 1 (e.g. ..."0"=>{"account_transfer_role"=>"source"...). Now, I have been working under the assumption (which I thought might come back to bite me, and it has) that because of the order they are built in the new action, that the source transfer_account would always have an id key of 0 and the destination transfer_account would always have an id key of 1, which led me to use these id keys elsewhere in the controller as though 0 represented source and 1 represented destination.
And all seemed to be working fine until I was trying different permutations of creating new or using existing accounts, creating new or editing existing transfers when suddenly the form appears with destination listed first and source second, which hadn't occurred before, causing the entries to now have destination associated with 0 and source associated with 1, breaking the code in the controller referred to above.
To make it clearer, here is the form:
#transfer_form
= simple_form_for #transfer do |t|
.form-inputs
= t.input :name, label: 'Transfer Name'
= t.input :description, required: false, label: 'Transfer Description'
= t.simple_fields_for :transfer_accounts do |ta|
- role = ta.object.account_transfer_role.titleize
= ta.input :account_transfer_role, as: :hidden
= ta.input :account_id, collection: #valid_accounts,
include_blank: 'Select account...',
label: "#{ role } Account",
error: 'Account selection is required.'
.account_fields{id: "#{ role.downcase }_account_fields"}
= ta.simple_fields_for :account do |a|
= a.input :bank_id, collection: #valid_banks,
include_blank: 'Select bank...',
label: "#{ role } Bank",
error: 'Bank selection is required.',
class: "#{ role.downcase }_account_input_field"
= a.input :name, label: "#{ role } Account Name",
class: "#{ role.downcase }_account_input_field"
= a.input :description, required: false,
label: "#{ role } Account Description",
class: "#{ role.downcase }_account_input_field"
= a.input :user_name, label: "#{ role } Account User Name",
class: "#{ role.downcase }_account_input_field"
= a.input :password, label: "#{ role } Account Password",
class: "#{ role.downcase }_account_input_field"
= a.input :account_number, label: "#{ role } Account Number",
class: "#{ role.downcase }_account_input_field"
= a.input :routing_number, label: "#{ role } Account Routing Number",
class: "#{ role.downcase }_account_input_field"
= t.submit
How do I ensure that source is always first and, thus, always associated with the id key 0 and destination is always second, always associated with the id key 1?
The answer appears to be as simple as changing the :transfer_accounts association line in my Transfer model from this:
has_many :transfer_accounts, inverse_of: :transfer
to this:
has_many :transfer_accounts, -> { order('account_transfer_role DESC') }, inverse_of: :transfer
If anyone believes it is not doing what I think it is doing, please let me know, because, at the moment it appears to be resolving my issue.
Related
I am facing an issue while learning Elixir & Ecto. The idea is to build a standard posts/comments page to understand how the basics work. I am at a point where I have schemas defined, a migration written and encounter an error when trying to insert data into the database (PostgreSQL) via the Repo. I have done a fair deal of web searching and documentation reading, which leads me to believe it's a scenario that should just work and I am making a stupid mistake somewhere, which I just can't see.
They are defined as follows:
lib/hello/schemas.ex
defmodule Hello.PostAuthor do
use Ecto.Schema
schema "post_authors" do
field :name, :string
end
end
defmodule Hello.CommentAuthor do
use Ecto.Schema
schema "comment_authors" do
field :name, :string
end
end
defmodule Hello.Comment do
use Ecto.Schema
schema "comments" do
has_one :author, Hello.CommentAuthor
field :date, :date
field :body, :string
end
end
defmodule Hello.Post do
use Ecto.Schema
schema "posts" do
has_one :author, Hello.PostAuthor
field :date, :date
field :body, :string
has_many :comments, Hello.Comment
end
end
as you can see, I have two fields with :date type - on post and comment schemas. The corresponding migration is as follows:
defmodule Hello.Repo.Migrations.CreatePosts do
use Ecto.Migration
def change do
create table(:post_authors) do
add :name, :string
end
create table(:comment_authors) do
add :name, :string
end
create table(:comments) do
add :author, references(:comment_authors)
add :date, :date
add :body, :string
end
create table(:posts) do
add :author, references(:post_authors), null: false
add :date, :date
add :body, :string
add :comments, references(:comments)
timestamps()
end
end
end
Now, when I start iex -S mix I can successfully create all structs:
iex(1)> post_author = %Hello.PostAuthor{name: "John"}
%Hello.PostAuthor{
__meta__: #Ecto.Schema.Metadata<:built, "post_authors">,
id: nil,
name: "John"
}
iex(2)> comment_author = %Hello.PostAuthor{name: "Adam"}
%Hello.PostAuthor{
__meta__: #Ecto.Schema.Metadata<:built, "post_authors">,
id: nil,
name: "Adam"
}
iex(3)> comment = %Hello.Comment{author: comment_author, date: ~D[2019-01-01], body: "this is a comment"}
%Hello.Comment{
__meta__: #Ecto.Schema.Metadata<:built, "comments">,
author: %Hello.PostAuthor{
__meta__: #Ecto.Schema.Metadata<:built, "post_authors">,
id: nil,
name: "Adam"
},
body: "this is a comment",
date: ~D[2019-01-01],
id: nil
}
iex(4)> post = %Hello.Post{author: post_author, date: ~D[2019-01-01], body: "this is a post", comments: [comment]}
%Hello.Post{
__meta__: #Ecto.Schema.Metadata<:built, "posts">,
author: %Hello.PostAuthor{
__meta__: #Ecto.Schema.Metadata<:built, "post_authors">,
id: nil,
name: "John"
},
body: "this is a post",
comments: [%Hello.Comment{
__meta__: #Ecto.Schema.Metadata<:built, "comments">,
author: %Hello.PostAuthor{
__meta__: #Ecto.Schema.Metadata<:built, "post_authors">,
id: nil,
name: "Adam"
},
body: "this is a comment",
date: ~D[2019-01-01],
id: nil
}],
date: ~D[2019-01-01],
id: nil
}
The problem arises when I call Hello.Repo.insert(post) (where post is the struct representing the Hello.Post schema). I receive what looks like serialization error:
iex(8)> Hello.Repo.insert(post) [debug] QUERY OK db=0.1ms
begin []
[debug] QUERY ERROR db=1.6ms
INSERT INTO "posts" ("body","date") VALUES ($1,$2) RETURNING "id" ["this is a post", ~D[2019-01-01]]
[debug] QUERY OK db=0.1ms
rollback []
** (DBConnection.EncodeError) Postgrex expected a binary, got ~D[2019-01-01]. Please make sure the value you are passing matches the definition in your table or in your query or convert the value accordingly.
(postgrex) lib/postgrex/type_module.ex:897: Postgrex.DefaultTypes.encode_params/3
(postgrex) lib/postgrex/query.ex:75: DBConnection.Query.Postgrex.Query.encode/3
(db_connection) lib/db_connection.ex:1148: DBConnection.encode/5
(db_connection) lib/db_connection.ex:1246: DBConnection.run_prepare_execute/5
(db_connection) lib/db_connection.ex:540: DBConnection.parsed_prepare_execute/5
(db_connection) lib/db_connection.ex:533: DBConnection.prepare_execute/4
(postgrex) lib/postgrex.ex:198: Postgrex.query/4
(ecto_sql) lib/ecto/adapters/sql.ex:666: Ecto.Adapters.SQL.struct/10
(ecto) lib/ecto/repo/schema.ex:651: Ecto.Repo.Schema.apply/4
(ecto) lib/ecto/repo/schema.ex:262: anonymous fn/15 in Ecto.Repo.Schema.do_insert/4
(ecto) lib/ecto/repo/schema.ex:916: anonymous fn/3 in Ecto.Repo.Schema.wrap_in_transaction/6
(ecto_sql) lib/ecto/adapters/sql.ex:898: anonymous fn/3 in Ecto.Adapters.SQL.checkout_or_transaction/4
(db_connection) lib/db_connection.ex:1415: DBConnection.run_transaction/4
This is where I am lost. Both the schema and the migration are expecting a :date . I believe that ~D[2019-01-01] is a date. PostgreSQL defines date as a 4 byte binary value. I am expecting Ecto.Adapters.Postgres to translate elixir date struct into the Postgres binary value. This is not happening. Why?
Struct itself is just raw data. You should go through Ecto.Changeset as shown in the documentation, specifically to all types to be cast to the respective DB types with Ecto.Changeset.cast/4.
The conversion will be done automagically, but you need to explicitly call cast/4 (hence the Changeset,) otherwise the adapter has no idea of how to convert your ecto types.
I'm trying to add an index for a for column but I'm getting the error:
ActiveRecord::StatementInvalid: PG::UndefinedColumn: ERROR: column "contact" does not exist
: CREATE UNIQUE INDEX "index_users_on_contact" ON "users" ("contact")
which is strange because as you an see I'm creating the column before I try to index it:
class AddContactToUser < ActiveRecord::Migration[5.1]
def change
add_reference :users, :contact, foreign_key: true
add_index :users, :contact, unique: true
end
end
Why am I getting this error?
In case your wondering why I'm doing a separate contact model, it's because all users will have a contact but not all contacts will have a user.
add_reference :users, :contact, foreign_key: true
Creates a column named contact_id. So you're index needs to be
add_index :users, :contact_id, unique: true
I am trying to build a FactoryGirl factory for the Client.rb model:
Client.rb
enum status: [ :unregistered, :registered ]
has_many :quotation_requests
#Validations
validates :first_name,
presence: true,
length: {minimum: 2}
validates :last_name,
presence: true,
length: {minimum: 2}
validates :email, email: true
validates :status, presence: true
Factory:
FactoryGirl.define do
factory :client do
first_name "Peter"
last_name "Johnson"
sequence(:email) { |n| "peterjohnson#{n}#example.com" }
password "somepassword"
status "unregistered"
end
end
client_spec.rb
require 'rails_helper'
RSpec.describe Client, type: :model do
describe 'factory' do
it "has a valid factory" do
expect(FactoryGirl.build(:client).to be_valid
end
end
end
I get the following errorL
1) Client factory has a valid factory
Failure/Error: expect(FactoryGirl.build(:client, status: 'unregistered')).to be_valid
expected #<Client id: nil, email: "peterjohnson1#example.com", encrypted_password: "$2a$04$urndfdXNfKVqYB5t3kERZ.c.DUitIVXEZ6f19FNYZ2C...", first_name: "Peter", last_name: "Johnson", status: "0", 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: nil, updated_at: nil> to be valid, but got errors: Status can't be blank
The error is that Status can't be blank.
I don't understand how this is possible as the factory is clearly assigning a value to the status attribute.
How can I get this factory to build a valid client object?
Rails 4.2
Using factory_girl 4.7.0
Using factory_girl_rails 4.7.0
This error was caused by the data type I used for the status attribute. I chose string instead of integer.
I solved the problem by running a new migration to change the data type of the status to integer.
class ChangeColumnTypeClientStatus < ActiveRecord::Migration
def change
change_column :clients, :status, :integer, default: 0
end
end
Now it works perfectly.
I think that you forgot the
let(:client) { FactoryGirl.create(:client) }
on your client_spec.rb
Where're you creating the client object?
Other issue may be that you assign on Factory:
status "unregistered"
instead of:
status :unregistered
as a symbol or due to is an enum maybe you should make
status 0 # :unregistered
I'm working through the Rails 4 version of Michael Hartl's Rails Tutorial and having trouble with section 9.6 exercise 1 (Listing 9.49).
It looks like the test in the tutorial passes for the wrong reason. Before the PATCH request, user.admin? is false by default; after the PATCH request user.admin? is still false (thus passing the test) because the PATCH request is not getting to the UsersController#update method.
Here's my code:
spec/requests/user_pages_spec.rb (other tests removed to isolate the one in question):
require 'spec_helper'
describe "User pages" do
subject { page }
describe 'edit' do
let(:user) { FactoryGirl.create(:user) }
before do
sign_in user
visit edit_user_path(user)
end
describe "forbidden attributes" do
let(:params) do
{ user: { name: 'Forbidden Attributes',
password: user.password,
password_confirmation: user.password,
admin: true } }
end
before { patch user_path(user), params }
specify { expect(user.reload).not_to be_admin }
end
end
end
Relevant parts of app/controllers/users_controller.rb:
class UsersController < ApplicationController
before_action :signed_in_user, only: [:index, :edit, :update]
before_action :correct_user, only: [:edit, :update]
# PATCH /users/:id
def update
# #user is set in before_action
if #user.update_attributes(user_params)
# handle a successful update
flash[:success] = 'Profile updated'
sign_in #user
redirect_to #user
else
render 'edit'
end
end
private
def user_params
params.require(:user).permit(:name, :email, :password,
:password_confirmation, **:admin**)
end
# Before filters
def signed_in_user
unless signed_in?
store_location
redirect_to signin_url, notice: 'Please sign in.'
end
end
def correct_user
#user = User.find(params[:id])
redirect_to(root_url) unless current_user?(#user)
end
end
spec/factories.rb:
FactoryGirl.define do
factory :user do
sequence(:name) { |n| "Person #{n}" }
sequence(:email) { |n| "person_#{n}#example.com" }
password "foobar"
password_confirmation "foobar"
factory :admin do
admin true
end
end
end
And here's what the test log shows happens:
Started PATCH "/users/2111" for 127.0.0.1 at 2013-08-18 21:30:44 -0400
Processing by UsersController#update as HTML
Parameters: {"user"=>{"name"=>"Forbidden Attributes", "password"=>"[FILTERED]", \
"password_confirmation"=>"[FILTERED]", "admin"=>"true"}, "id"=>"2111"}
User Load (0.4ms) SELECT "users".* FROM "users" WHERE "users"."remember_token" = \
'da39a3ee5e6b4b0d3255bfef95601890afd80709' LIMIT 1
**Redirected to http://www.example.com/signin
Filter chain halted as :signed_in_user rendered or redirected**
Completed 302 Found in 2ms (ActiveRecord: 0.4ms)
I downloaded the reference version of the code from https://github.com/railstutorial/sample_app_rails_4 and ran rspec spec/requests/user_pages_spec.rb. The test log showed the same thing: the PATCH request is being stopped by signed_in_user and never getting to the update method.
When I played around with testing that admin IS set and added some puts statements, it looked like the user that is being signed in is not the same user that is being tested; the user.id stays constant, but the user.name changes. I wonder if it has something to do with the sequence() calls in the factory.
Can anyone verify or refute my findings?
How can I write this test properly?
Found solution
Further investigation seemed to implicate the remember_token. If I move the "forbidden attributes" test out of the "edit" block and add "capybara: true" to the sign_in call, it works. So Listing 9.49 (spec/requests/user_pages_spec.rb) should look like this:
require 'spec_helper'
describe "User pages" do
subject { page }
.
.
.
describe "update forbidden attributes" do
let(:user) { FactoryGirl.create(:user) }
let(:params) do
{ user: { admin: true, password: user.password,
password_confirmation: user.password } }
end
before do
sign_in user, no_capybara: true
patch user_path(user), params
end
specify { expect(user.reload).not_to be_admin }
end
end
I just wanted to confirm that I'm seeing the same behavior when the "forbidden attributes" test is nested within the "edit" test block.
My notes indicate that it was mentioned in Chapter 9 that when ever you're doing a direct POST, PATCH, GET or DELETE request, as opposed to using visit, the no_capybara: true option needs to be given to the sign_in method to ensure the user is signed in.
However, in this case if you use the no_capybara: true option with sign_in the other tests in the "edit" block will fail due to some Capybara issues.
As mentioned by the OP, if the option is omitted, then the "forbidden attributes" tests pass regardless of the presence, or lack thereof, of :admin in the user_params method within the Users controller.
Same problem here. With metafour's help, I find the following works. We need to sign in the user with capybara: true for patch to work.
describe "forbidden attributes" do
let(:params) do
{ user: { admin: true, password: user.password,
password_confirmation: user.password } }
end
before do
sign_in user, no_capybara: true
patch user_path(user), params
end
specify { expect(user.reload).not_to be_admin }
end
I'm trying to follow along with http://mongotips.com/b/array-keys-allow-for-modeling-simplicity/
I have a Story document and a Rating document. The user will rate a story, so I wanted to create a many relationship to ratings by users as such:
class StoryRating
include MongoMapper::Document
# key <name>, <type>
key :user_id, ObjectId
key :rating, Integer
timestamps!
end
class Story
include MongoMapper::Document
# key <name>, <type>
timestamps!
key :title, String
key :ratings, Array, :index => true
many :story_ratings, :in => :ratings
end
Then
irb(main):006:0> s = Story.create
irb(main):008:0> s.ratings.push(Rating.new(user_id: '0923ksjdfkjas'))
irb(main):009:0> s.ratings.last.save
=> true
irb(main):010:0> s.save
BSON::InvalidDocument: Cannot serialize an object of class StoryRating into BSON.
from /usr/local/lib/ruby/gems/1.9.1/gems/bson-1.6.2/lib/bson/bson_c.rb:24:in `serialize' (...)
Why?
You should be using the association "story_rating" method for your push/append rather than the internal "rating" Array.push to get what you want to follow John Nunemaker's "Array Keys Allow For Modeling Simplicity" discussion. The difference is that with the association method, MongoMapper will insert the BSON::ObjectId reference into the array, with the latter you are pushing a Ruby StoryRating object into the Array, and the underlying driver driver cant serialize it.
Here's a test that works for me, that shows the difference. Hope that this helps.
Test
require 'test_helper'
class Object
def to_pretty_json
JSON.pretty_generate(JSON.parse(self.to_json))
end
end
class StoryTest < ActiveSupport::TestCase
def setup
User.delete_all
Story.delete_all
StoryRating.delete_all
#stories_coll = Mongo::Connection.new['free11513_mongomapper_bson_test']['stories']
end
test "Array Keys" do
user = User.create(:name => 'Gary')
story = Story.create(:title => 'A Tale of Two Cities')
rating = StoryRating.create(:user_id => user.id, :rating => 5)
assert_equal(1, StoryRating.count)
story.ratings.push(rating)
p story.ratings
assert_raise(BSON::InvalidDocument) { story.save }
story.ratings.pop
story.story_ratings.push(rating) # note story.story_ratings, NOT story.ratings
p story.ratings
assert_nothing_raised(BSON::InvalidDocument) { story.save }
assert_equal(1, Story.count)
puts Story.all(:ratings => rating.id).to_pretty_json
end
end
Result
Run options: --name=test_Array_Keys
# Running tests:
[#<StoryRating _id: BSON::ObjectId('4fa98c25e4d30b9765000003'), created_at: Tue, 08 May 2012 21:12:05 UTC +00:00, rating: 5, updated_at: Tue, 08 May 2012 21:12:05 UTC +00:00, user_id: BSON::ObjectId('4fa98c25e4d30b9765000001')>]
[BSON::ObjectId('4fa98c25e4d30b9765000003')]
[
{
"created_at": "2012-05-08T21:12:05Z",
"id": "4fa98c25e4d30b9765000002",
"ratings": [
"4fa98c25e4d30b9765000003"
],
"title": "A Tale of Two Cities",
"updated_at": "2012-05-08T21:12:05Z"
}
]
.
Finished tests in 0.023377s, 42.7771 tests/s, 171.1084 assertions/s.
1 tests, 4 assertions, 0 failures, 0 errors, 0 skips