I cleaned up my code, it looks much nicer now, but still doesn’t work. It starts to be a pain…
I just can’t save a parent with an existing child in nested form with parent has_many childs :through joinmodel.
In my case a Project has_many contributing Teachers and many contributing Pupils, both are Join-Models to Users. A Project has_many Schools as well.
(May be I should better name the models Teacherize and Pupilize or ProjectTeacher and ProjectPupil.)
As long as all records are new it all works fine. As soon as I want to connect an existing User as new Teacher of a new Project I get the following error:
Couldn't find User with ID=1 for Teacher with ID=
(1 is the correct user ID)
The problem should be somewhere her in my helper to setup empty form fields:
At least I guess so...
module ProjectsHelper
def setup_project(project)
if project.teachers.length <= 0 # usually there is just one teacher, so add one if there isn't one
teacher = project.teachers.build(:role_in_project => 'master')
if user_signed_in?
#teacher = project.teachers.new(:role_in_project => 'master', :user => current_user)
teacher.user = current_user # associate first teacher with current_user
else
#teacher = project.teachers.build
teacher.user = User.new # associate first teacher with a new user instance
end
end
if project.project_schools.length <= 0 # usually there is just one school, so add one if there isn't one
project_school = project.project_schools.build
project_school.school = School.new
end
if project.pupils.length < 3 # There can be up to 3 people, so add a blank fieldset as long as there are less than 3
pupil = project.pupils.build
pupil.user = User.new
end
project
end
end
These are my params received:
{"utf8"=>"✓", "authenticity_token"=>"uCCMk/s3SpDfR7+fXcsCOHPvfvivBQv8pVFVhdh6iro=",
"project"=>{
"teachers_attributes"=>{
"0"=>{
"id"=>"",
"user_attributes"=>{
"id"=>"1",
"gender"=>"male",
"title"=>"",
"firstname"=>"Firstname1",
"name"=>"Lastname1",
"faculty"=>"",
"fon"=>"",
"fax"=>""}
}
},
"id"=>"",
"title"=>"First Project",
"description"=>"This is a foo bar project!",
"presentation_type"=>"experimentell",
"note"=>""
},
"commit"=>"Register Project",
"action"=>"create",
"controller"=>"projects"
}
The case isn’t too abstract; it has to be possible to achieve it.
It’s just to connect a new parent record with an existing child record!
In this article, which is very good, exactly the case is explaint:
http://rubysource.com/complex-rails-forms-with-nested-attributes
# app/helpers/form_helper
module FormHelper
def setup_user(user)
user.address ||= Address.new
(Interest.all - user.interests).each do |interest|
user.interest_users.build(:interest => interest)
end
user.interest_users.sort_by! {|x| x.interest.name }
user/tmp/clean-controllers.md.html
end
end
There the interest is existing and gets connected through a new record in interest_uesers.
Why do I get the error when trying to do the same thing?
project.teachers.build(:user => current_user)
I studied several articles and casts, but none of them connect existing childs.
http://railscasts.com/episodes/196-nested-model-form-revised
http://apidock.com/rails/ActionView/Helpers/FormHelper/fields_for
http://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html
Rails 3.1+ Nested Forms Issue: Can't mass-assign protected attributes
Trying to use accepts_nested_attributes_for and has_and_belongs_to_many but the join table is not being populated
Quote: “accepts_nested_fields_for is used to create and modify related objects in a form. It can be used to populate join table, which is kind of what you're trying to do. However, using accepts_nested_fields_for to populate the join table is impossible with a HABTM relationship.”
That’s what I wanna do! Populate the join table!
It starts to be frustrating and I’d be glad to get some help!
My Models
class Project < ActiveRecord::Base
attr_accessible :title, :description, :presentation_type, :note,
:project_schools_attributes, :schools_attributes, :teachers_attributes, :pupils_attributes,
:users_attributes
validates_presence_of :title
validates_presence_of :description
validates_presence_of :presentation_type
has_many :project_schools, :dependent => :destroy
accepts_nested_attributes_for :project_schools
has_many :schools, :through => :project_schools
#accepts_nested_attributes_for :schools, :reject_if => :all_blank
has_many :pupils, :dependent => :destroy
accepts_nested_attributes_for :pupils, :reject_if => :all_blank
has_many :users, :through => :pupils
has_many :teachers, :dependent => :destroy
accepts_nested_attributes_for :teachers, :reject_if => :all_blank
has_many :users, :through => :teachers
#accepts_nested_attributes_for :users, :reject_if => :all_blank
end
class ProjectSchool < ActiveRecord::Base
attr_accessible :role_in_project, :comment,
:school_attributes, :school_id, :project_id
belongs_to :school
accepts_nested_attributes_for :school
belongs_to :project
end
class School < ActiveRecord::Base
attr_accessible :email, :fax, :fon, :name, :place, :street, :type_of_school, :www, :zip
has_many :project_schools
has_many :projects, :through => :project_schools
has_many :users # in real live they are named teachers and pupils but in this case the association goes directly to a user_id, not to teacher/pupil model
validates_presence_of :name, :type_of_school, :street, :place, :zip, :fon
validates :email, :format => { :with => /\A([^#\s]+)#((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i, :on => :create }, :allow_blank => true
end
class Teacher < ActiveRecord::Base
attr_accessible :role_in_project, :user, :user_attributes, :project_id, :user_id
belongs_to :project
belongs_to :user
accepts_nested_attributes_for :user
serialize :role_in_project
end
class Pupil < ActiveRecord::Base
attr_accessible :classname, :user_attributes #, :project_id, :user_id
belongs_to :project
belongs_to :user
accepts_nested_attributes_for :user
end
class User < ActiveRecord::Base
serialize :roles
belongs_to :school
has_many :teachers
has_many :pupils
has_many :projects, :through => :teachers
has_many :projects, :through => :pupils
# Setup accessible (or protected) attributes for your model
attr_accessible :email, :gender, :firstname, :name, :street, :place, :title, :faculty, :assignment,
:classname, :zip, :fon, :fax, :school_id, :roles,
:password, :added_by_user_id, :password_confirmation, :remember_me
# Include default devise modules. Others available are:
# :token_authenticatable, :confirmable,
# :lockable, :timeoutable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable#, :validatable
after_initialize :init
def init
# the default guest user
self.roles ||= ['default'] #will set the default value only if it's nil
end
end
My controller
class ProjectsController < ApplicationController
require 'axlsx'
before_filter :load_page, only: [:show, :index, :destroy]
def new
#project = Project.new
end
def create
#project = Project.new(params[:project])
respond_to do |format|
if #project.save
sign_in(:user, #project.teachers[0].user) unless user_signed_in?
# TODO: send mail
# save as excel file in dropbox
save_in_dropbox(#project)
format.html { redirect_to #project, notice: t('project.was_created') }
else
logger.debug #project.errors.inspect
format.html { render action: "new" }
end
end
end
end
projects/_form.html.haml
%h1
= t('project.register_headline')
= simple_form_for( setup_project(#project), :html => {:class => 'form-horizontal'} )do |f|
= f.error_notification
#teachers-wrapper.well
%span.jumpanchor#lehrkraft_anchor
%fieldset.form-inputs
= f.simple_fields_for :teachers do |teacher|
= render "teacher_fields", :f => teacher
.school-wrapper.well
%span.jumpanchor#schule_anchor
%h2
Informationen zur Schule
%fieldset.form-inputs
= f.simple_fields_for :project_schools do |project_school|
= render "school_fields", :f => project_school
.project-wrapper.well
%span.jumpanchor#projekt_anchor
%h2
Informationen zum Projekt der Schüler
%fieldset.form-inputs
= f.hidden_field :id
= f.input :title, :input_html => { :class => 'span6' }
= f.input :description, :input_html => { :class => 'span6', rows: 5 }
= f.input :presentation_type, collection: ['theoretisch', 'experimentell'], as: :radio_buttons, :class => 'controls-row', :input_html => { :class => 'inline' }
.clearfix
= f.input :note, :input_html => { :class => 'span6', rows: 3 }
.pupils-wrapper.well
%span.jumpanchor#schuler_anchor
%fieldset.form-inputs
= f.simple_fields_for :pupils do |pupil|
= render "pupil_fields", :f => pupil
projects/_teacher_fields.html.haml
= f.simple_fields_for :user do |user|
=# render "teacher_user_fields", :f => user
%h2
Betreuende Lehrkraft
- if user_signed_in?
= user.input :email, :disabled => true, :input_html => {:class => 'email_validation'}
- else
= user.input :email, :autofocus => true, :input_html => {:class => 'email_validation'}, :hint => 'Dies muß Ihre eigene E-Mailadresse sein!'
.details
=# user.hidden_field :id
= user.input :id
= user.input :gender, collection: [:female, :male]
= user.input :title
= user.input :firstname
= user.input :name
= user.input :faculty
= user.input :fon
= user.input :fax
It is Rails 3.2 with Ruby 1.9.3
I have to model's where I accept Nested Attributes. I would like to build a test to make sure the nested attribute cant be blank etc. I really don't understand how I can make the test.
My two simple models:
# SeoMapping Model
class SeoMapping < ActiveRecord::Base
belongs_to :mappingtable, :polymorphic => true
attr_accessible :seo_url
validates :seo_url, :presence => true, :uniqueness => true
end
# Page Model
class Page < ActiveRecord::Base
has_one :seo_mappings, :as => :mappingtable, :dependent => :destroy
accepts_nested_attributes_for :seo_mappings
attr_accessible :content, :h1, :meta_description, :title, :seo_mappings_attributes
.........
end
Here are my factories for Page and Seo:
FactoryGirl.define do
factory :page do |f|
seo_mapping
f.title { Faker::Name.name }
f.h1 { Faker::Lorem.words(5) }
f.meta_description { Faker::Lorem.words(10) }
f.content { Faker::Lorem.words(30) }
end
end
FactoryGirl.define do
factory :seo_mapping do |f|
f.seo_url { Faker::Internet.domain_word }
end
end
And my tests:
require 'spec_helper'
describe Page do
it "has a valid factory" do
expect(create(:page)).to be_valid
end
# Cant get this spec to work?
it "it is invalid without a seo_url" do
page = build(:page)
seo_mapping = build(:seo_mapping, seo_url: nil)
page.seo_mapping.should_not be_valid
# expect(build(:page, :seo_mapping_attributes[:seo_url] => nil)).to_not be_valid
end
it "is invalid without a title" do
expect(build(:page, title: nil)).to_not be_valid
end
...............
end
Usually for this sort of thing I use a gem called shoulda_matchers. It lets you simply assert that your model validates presence of specific attributes.
it { should validate_presence_of(:seo_url) }
it { should validate_uniqueness_of(:seo_url) }
If you don't want to use the gem, try something like this:
seo_mapping = build(:seo_mapping, seo_url: nil)
page = build(:page, seo_mapping: seo_mapping)
page.should_not be_valid
I'm not able to access the values, passed as option in 'validates_with'
My model:
class Person < ActiveRecord::Base
include ActiveModel::Validations
attr_accessible :name, :uid
validates :name, :presence => "true"
validates :uid, :presence => "true"
validates_with IdValidator, :attr => :uid
My Custom Validator:
Class IdValidator < ActiveModel::Validator
def validate(record)
puts options[:attr]
...
...
end
end
For testing purpose, I'm printing "options[:attr]" and all I see is ":uid" in the terminal and not the value in it. Please help!
When you pass in :attr => :uid, you're just passing in a symbol. There's no magic happening here—it just takes the hash of options you've attached and delivers it as the options hash. So when you write it, you see the symbol you've passed.
What you probably want is
Class IdValidator < ActiveModel::Validator
def validate(record)
puts record.uid
...
...
end
end
Because validates_with is a class method, you can't get the values of an individual record in the options hash. If you are interested in a more DRY version, you could try something like:
class IdValidator < ActiveModel::Validator
def validate(record)
puts record[options[:field]]
end
end
class Person < ActiveRecord::Base
include ActiveModel::Validations
attr_accessible :name, :uid
validates :name, :presence => "true"
validates :uid, :presence => "true"
validates_with IdValidator, :field => :uid
end
Where you pass in the name of the field you want evaluated.
User has two addresses shipping(:address_type=0) and billing(:address_type=1)
User form with 2 classic nested forms for each address type are generated square times every submit and failed validation.
Models:
class User < ActiveRecord::Base
has_many :addresses, :dependent => :destroy
accepts_nested_attributes_for :addresses
validates_associated :addresses
end
class Address < ActiveRecord::Base
belongs_to :user
validates :user, :address_type, :first_name, :last_name, :street
end
Controller
class UsersController < ApplicationController
public
def new
#user = User.new
#shipping_address = #user.addresses.build({:address_type => 0})
#billing_address = #user.addresses.build({:address_type => 1})
end
def create
#user = User.new(params[:user])
if #user.save
#fine
else
render => :new
end
end
Uncomplete Form
=form_for #user, :html => { :multipart => true } do |ff|
=ff.fields_for :addresses, #shipping_address do |f|
=f.hidden_field :address_type, :value => 0
=ff.fields_for :addresses, #billing_address do |f|
=f.hidden_field :address_type, :value => 1
=ff.submit
The form should look like this:
=form_for #user, :html => { :multipart => true } do |ff|
=ff.fields_for :addresses do |f|
Nothing else.
Addressess is already a collection, so you should have just one rendering of it.
Also that ":addresses, #shipping_address" makes it to render addresses AND shipping address, even if it's included in #user.addresses.
The addressess built in new action will show there because they are in the addresses collection.
EDIT:
If you need only these two addresses, you can sort it and pass it to fields_for directly:
=form_for #user, :html => { :multipart => true } do |ff|
=ff.fields_for ff.object.addresses.sort{|a,b| a.address_type <=> b.address_type } do |f|
That should do it.
Surprised? I guess not but I was. I found it am I correct? And its stupid and simple.
There is no #shipping_address nor #billing_address when validation fails and rendering the new action (the form) again. But #user has already 2 addresses builded and nested form behave correctly to render each twice for first time failed validation.
def create
#user = User.new(params[:user])
if #user.save
#fine
else
#user.addresses.clear
#user_address = #user.addresses.build({:address_type => 0})
#user_address.attributes = params[:user][:addresses_attributes]["0"]
#billing_address = #user.addresses.build({:address_type => 1})
#billing_address.attributes = params[:user][:addresses_attributes]["1"]
render => :new
end
end
I am having the following model class on ActiveRecord. How to write an equivalent ActiveModel for this class?
class Recommendation < ActiveRecord::Base
def self.columns() #columns ||= []; end
def self.column(name, sql_type = nil, default = nil, null = true)
columns << ActiveRecord::ConnectionAdapters::Column.new(name.to_s, default, sql_type.to_s, null)
end
column :from_email, :string
column :to_email, :string
column :article_id, :integer
column :message, :text
serialize :exception
validates_format_of :from_email, :to_email, :with => /^[-a-z0-9_+\.]+\#([-a-z0-9]+\.)+[a-z0-9]{2,4}$/i
validates_length_of :message, :maximum => 500
belongs_to :article
end
I suggest you start with a plain class, and then start adding in ActiveModel modules. Say, start with validation.
http://api.rubyonrails.org/classes/ActiveModel/Validations.html
class Recommendation
include ActiveModel::Validations
attr_accessor :from_email, :to_email, :article_id, :message
validates_format_of :from_email, :to_email, :with => /^[-a-z0-9_+\.]+\#([-a-z0-9]+\.)+[a-z0-9]{2,4}$/i
validates_length_of :message, :maximum => 500
end
The other ActiveModel docs can be found at http://api.rubyonrails.org/