Nested forms with has_one :through - ruby-on-rails

I'm having some trouble to implement a nested form with a has_one :through association.
Models
# model: member.rb
belongs_to :user
has_one :academic
# model: user.rb
has_one :member
has_one :academic, through: :member
accepts_nested_attributes_for :member, reject_if: :all_blank
accepts_nested_attributes_for :academic, reject_if: :all_blank
# model: academic.rb
belongs_to :member
belongs_to :user
Controller
# users_controller.rb
def new
#user = User.new
#user.build_member
#user.build_academic
end
I also have tried with:
#user.member.build_academic
View
# new.html.erb
<%= simple_form_for(resource, as: resource_name, url: registration_path(resource_name)) do |ff| %>
<%= ff.text_field :email %>
# member belongs to user so I can call a fields_for
<% ff.fields_for :member do |f| %>
<%= f.text_field :name %>
# this part is not shown. What is wrong with my association?
<% f.fields_for :academic do |a| %>
<%= a.text_field :major %>
<% end %>
<% end %>
<% end %>
I've taken a look into the Rails documentation. The first fields_for is shown in the page (:member), but the second one (:academic), which has the has_one :through association, is not shown in the page.
Any help will be appreciated. Thank you.

Through
If you want to build your data through a relation, you have to pass the associated data as it's constructed:
#app/models/member.rb
class Member < ActiveRecord::Base
belongs_to :user
has_one :academic
accepts_nested_attributes_for :academic
end
#app/models/user.rb
class User < ActiveRecord::Base
has_one :member
has_one :academic, through: :member
accepts_nested_attributes_for :member
end
#app/models/academic.rb
class Academic < ActiveRecord::Base
belongs_to :member
belongs_to :user
end
This will allow you to do the following:
#app/controllers/members_controller.rb
class MembersController < ApplicationController
def new
#member = Member.new
#member.build_member.build_academic
end
def create
#member = Member.new member_params
#member.save
end
private
def member_params
params.require(:member).permit(:x, :y, :z, academic_attributes: [:some, :attributes, member_attributes:[...]])
end
end
This would permit the following:
#app/views/users/new.html.erb
<%= form_for #user do |f| %>
<%= f.fields_for :member do |m| %>
<%= f.text_field :name %>
...
<%= m.fields_for :academic do |a| %>
<% a.text_field :name %>
...
<% end %>
<% end %>
<%= f.submit %>
<% end %>
This works to build a new member object, and a new academic object from the user. Although not strictly what you're asking, it looks like it could benefit you in some form.
Associations
If you want to do what you're asking (IE build_member and build_academic exclusively), you'll need to get rid of the has_one :through relationship...
#app/models/user.rb
class User < ActiveRecord::Base
has_many :memberships
has_many :academics, through: :memberships
end
#app/models/membership.rb
class Membership < ActiveRecord::Base
belongs_to :user
belongs_to :academic
end
#app/models/academic.rb
class Academic < ActiveRecord::Base
has_many :memberships
has_many :users, through: :memberships
end
The problem is you're basically trying to build a relationship for a direct association (member) and an indirect relationship (academic).
If you want to build both exclusively, you have to make them have a direct association with your main model. The above should allow you to do the following:
#app/controllers/users_controller.rb
class UsersController < ApplicationController
def new
#user = User.new
#user.members.build.academics.build
end
end
Much like my top example, this will pass nested data through your form - if you wanted to have it completely exclusively, do this:
#app/models/user.rb
class User < ActiveRecord::Base
has_one :member
has_one :academic
has_and_belongs_to_many :academics
end
#app/models/member.rb
class Member < ActiveRecord::Base
belongs_to :user
end
#app/models/academic.rb
class Academic < ActiveRecord::Base
belongs_to :user
has_and_belongs_to_many :users
end
This will allow the following:
#app/controllers/users_controller.rb
class UsersController < ApplicationController
def new
#user = User.new
#user.build_member
#user.build_academic
end
def create
#user = User.new user_params
#user.save
end
private
def user_params
params.require(:user).permit(:user, :params, member_attributes: [], academic_attributes:[])
end
end

For future reference or for anyone that has the same problem, I found a way around to fix this problem, in case you have same structure.
I could fix this by going to the view, before the form, and writing:
<% resource.member.build_academic %>
In my case resource is User, set by devise, a Rails gem used for authentication.
And you don't need to reference any :through or whatsoever in your model.
It is not the most efficient way, but I haven't found any other solution. Hope it helps.

Related

Cannot access attributes from associated model rails

I have three models, ingredient, recipe_ingredient and recipy
class Ingredient < ApplicationRecord
has_many :recipe_ingredients
end
class RecipeIngredient < ApplicationRecord
belongs_to :recipy, :dependent => :destroy
belongs_to :ingredient
end
class Recipy < ApplicationRecord
has_many :recipy_steps
has_many :recipe_ingredients, :dependent => :delete_all
end
I am trying to access the ing_name attribute in the ingredients table from recipies show page.
<% #recipe_ingredients.each do |ing| %>
<p> <%= ing.amount %> <%= ing.unit %>
<%= ing.ingredient.ing_name %>
</p>
def Show from the recipies controller:
def show
#recipe_ingredients = #recipy.recipe_ingredients
end
But I keep receiving the following error msg:
undefined method `ing_name' for nil:NilClass
My ingredient_params:
def ingredient_params
params.require(:ingredient).permit(:ing_name)
end
It does seem to work like this:
<%= Ingredient.where(id: ing.ingredient_id).pluck(:ing_name) %>
But this does not use the connection between the tables if I understand correctly? Any help? Thanks.
You have ingredient nil thats why you got the error.
Must be your controller has some before_action hook to load recipy
class RecipesController < ApplicationController
before_action :load_recipy, only: :show
def show
#recipe_ingredients = #recipy.recipe_ingredients
end
private
def load_recipy
#recipy = Recipy.find(params[:id])
end
end
You can try this to avoid this nil error(undefined method 'ing_name' for nil:NilClass)
<% #recipe_ingredients.each do |ing| %>
<p> <%= ing.amount %> <%= ing.unit %>
<%= ing.try(:ingredient).try(:ing_name) %>
</p>
From Rails 5 by default you got one required option to make ingredient always not nullable
like below
belongs_to :ingredient, required: true
It will also prevent this error of
class RecipeIngredient < ApplicationRecord
belongs_to :recipy, :dependent => :destroy
belongs_to :ingredient, required: true
end
the problem is because inside your show method #recipy is nil,
here is usually code for show
controller
def show
#recipy = Recipy.find(params[:id]) # --> you missed this line
#recipe_ingredients = #recipy.recipe_ingredients # #recipy will be null without line above
end
view
<% #recipe_ingredients.each do |ing| %>
<p> <%= ing.amount %> <%= ing.unit %> <%= ing.ingredient.ing_name %> </p>
<% end %>
I would like also add some suggestion to your model relationship as follow since Ingredient and Recipy shows many to many relationship
class Ingredient < ApplicationRecord
# -> recipe_ingredients -> recipies
has_many :recipe_ingredients, :dependent => :destroy
has_many :recipies, through: :recipe_ingredients
end
class RecipeIngredient < ApplicationRecord
belongs_to :recipy
belongs_to :ingredient
end
class Recipy < ApplicationRecord
has_many :recipy_steps
# -> recipe_ingredients -> ingredients
has_many :recipe_ingredients, :dependent => :destroy
has_many :ingredients, through: :recipe_ingredients
end

Rails where to create :through object

Hi I`m learning has_many :through and I have a association like this.
student:
has_many :subjects, through: :participations
has_many :participations
subject:
has_many :students, through: :participations
has_many :participations
belongs_to :student
participation:
belongs_to :student
belongs_to :subject
The student subjects are updated through checkboxes in update view:
= f.association :subjecs, label_method: :title, value_method: :id, label: 'Subjects', as: :check_boxes
And I went so far :( My student have subjects id, but it can`t get them since no participation is created.
My update action:
def create
student = Student.new(student_params)
if student.save
redirect_to students_path
else
render 'edit'
end
end
My question is when should I create participation object, and where is the appropriate place for the function ?
When you have a has_many relationship, you actually get a bunch of methods which can help you.
One of these is association_singular_ids- which if you populate it correctly, will automatically create the associative data you need.
The way to do this will be to use the following:
#app/views/students/new.html.erb
<%= form_for #student do |f| %>
<%= f.collection_check_boxes :subject_ids, Subject.all, :id, :name %>
<%= f.submit %>
<% end %>
I know you're using f.association (which is built through simple_form) - you'll be much better suited to using collection_check_boxes (it even explains an example of what you're having problems with).
You shouldn't need to pass the params or anything - and because your participations model acts as a join, it should be populated automatically if you use the above code.
HABTM
You may also wish to look at has_and_belongs_to_many:
#app/models/student.rb
class Student < ActiveRecord::Base
has_and_belongs_to_many :subjects
end
#app/models/subject.rb
class Subject < ActiveRecord::Base
has_and_belongs_to_many :students
end
#join table - students_subjects
This is often preferred over has_many :through because it requires less maintenance (as described in the link above).
Nested Attributes
Finally, to give you some more perspective, you'll need to know about the nested attributes aspect of Rails.
I originally thought your answer would be that you're not using accepts_nested_attributes_for, but I don't think so now. Nonetheless, you'll still gain benefit from knowing about it.
--
One of the reasons you'd use has_many through would be to populate the join model with other attributes. HABTM does not allow this; because has_many :through has a join model (in your case participations), it allows you to add extra attributes into it.
As such, if you're looking to change any of those attributes, you'll need to pass them through your various models:
#app/models/student.rb
class Student < ActiveRecord::Base
has_many :participations
has_many :subjects, through: :participations
accepts_nested_attributes_for :participations
end
#app/models/participation.rb
class Participation < ActiveRecord::Base
belongs_to :student
belongs_to :subject
accepts_nested_attributes_for :student
end
#app/models/subject.rb
class Subject< ActiveRecord::Base
has_many :participations
has_many :students, through: :participations
end
This will allow you to use the following:
#app/controllers/students_controller.rb
class StudentsController < ApplicationController
def new
#student = Student.new
#student.participations.build
end
def create
#student = Student.new student_params
#student.save
end
private
def student_params
params.require(:student).permit(:student, :params, participations_attributes: [subject_attributes:[]])
end
end
This should allow you to use the following:
#app/views/students/new.html.erb
<%= form_for #student do |f| %>
<%= f.fields_for :participations do |p| %>
<%= p.text_field :name %>
<%= p.collection_check_boxes :subject_id, Subject.all, :id, :name %>
<% end %>
<%= f.submit %>
<% end %>
My problem was that I didn`t permited the chenge od subject_ids in the student controller:
params.require(:student).permit(:first_name, :last_name, subject_ids: [])

Rails conditional_select for nested attributes

I'm trying to setup the following: A User has many Groups through Memberships, a Group has many Events, and an Event has many Posts.
On my view to show a group with all of its events, I want a user to be able to write a new post by selecting the correct group from a drop down, writing a comment and submit. I'm currently using a collection_select to create the post, but the event_id is not getting passed to ActiveRecord, i.e. posts are created, but they do not have event_ids (or even comments):
class User < ActiveRecord::Base
has_many :memberships
has_many :groups, through: :memberships
has_many :posts
end
class Membership < ActiveRecord::Base
belongs_to :group
belongs_to :user
end
class Group < ActiveRecord::Base
has_many :memberships
has_many :events, dependent: :destroy
has_many :users, through: :memberships
end
class Event < ActiveRecord::Base
belongs_to :group
has_many :posts
end
class Post < ActiveRecord::Base
belongs_to :event
belongs_to :user
end
class GroupsController < ApplicationController
def show
#define new post
#new_post = Post.new
end
end
class PostsController < ApplicationController
def create
if #post = Post.create(params[post_params])
flash[:success] = "Post Created!"
else
redirect_to group_url
end
end
private
def post_params
params.require(:post).permit(:event_id, :comment)
end
end
<h1>New Post:</h1>
<%=form_for([#new_post]) do |f| %>
<%= render 'shared/error_messages', object: f.object %>
<div class = "field">
<%= f.label :event_name %>
<%= f.collection_select(:event_id, Event.all, :id, :title) %>
</div>
<div class = "field">
<%= f.text_area :comment, placeholder: "New Post..." %>
</div>
<%=f.submit "Submit", class: "btn btn-large btn-primary" %>
<%end%>
I have a feeling that because the routes are nested, group_id never is passed to the Posts controller, and so can never be set. But I'm sure there's a lot more wrong than that...
can you try to pass Post.create(post_params) instead of Post.create(params[post_params])
post_params is actually a full hash extracted from the params so you should not pass it to params again
If you want to add user_id
you should add to your view something like this
<%= f.hidden_field :user_id, value: current_user.id %>

rails nested form with has_many :through relationship

Here are my models:
class Project < ActiveRecord::Base
has_many :project_applications
has_many :questions
accepts_nested_attributes_for :questions, :allow_destroy => true, :reject_if => proc { |a| a[:content].blank? }
end
class Question < ActiveRecord::Base
belongs_to :project
has_many :answers
has_many :project_applications, through: :answers
end
class Answer < ActiveRecord::Base
belongs_to :question
belongs_to :project_application
end
class ProjectApplication < ActiveRecord::Base
belongs_to :project
belongs_to :student
has_many :answers
has_many :questions, through: :answers
end
A project is created by an employer, and a student can create a project_application. The project_application should present the questions and then show form fields that correspond to the questions answers. I cannot for the life of me figure out how the form view should look. I need a form_for ProjectApplication that accepts nested attributes for answers. I have the following in my controller:
class ProjectApplicationsController < ApplicationController
def new
#project = Project.find(params[:project_id])
#project_application = ProjectApplication.new
#project_application.project = #project
#project_application.project.questions.each do |question|
#answer = question.answers.build
#answer.project_application = #project_application #this line does not work
puts 'answer' + #answer.inspect.to_s
end
puts 'here are the answers' + #project_application.answers.inspect.to_s
end
end
The problem with this is that the answers are not correctly being associated with project_applications because the project_applications don't have an id yet (because they have not been created) so the association can't happen, so the answer fields are not displayed. Here is the view code (does not work) that I have now:
<%= form_for #project_application, url: project_project_applications_path(#project.id), method: :post, remote: true do |f| %>
<%= f.fields_for :project do |proj| %>
<%= proj.fields_for :questions do |quest| %>
<%= quest.fields_for :answers do |answer| %>
<%= answer.text_area :content %>
<% end %>
<% end %>
<% end %>
<%= f.submit "APPLY" %>
<% end %>
How do I change the view and/or controller to properly display answer fields correctly associated with questions and the project application?
My understanding:
You have projects
Each project has many questions & project_applications
Each question belongs to a project, has many answers through project_applications
For each project, you'd like to create applications with the applicable answers. Here's what I'd do:
Models
class Project < ActiveRecord::Base
has_many :project_applications
has_many :questions
end
class Question < ActiveRecord::Base
belongs_to :project
has_many :answers
has_many :project_applications, through: :answers
end
class Answer < ActiveRecord::Base
belongs_to :question
belongs_to :project_application
end
class ProjectApplication < ActiveRecord::Base
belongs_to :project
belongs_to :student
has_many :answers
has_many :questions, through: :project
accepts_nested_attributes_for :answers
def self.build
application = self.new
application.answers.build
end
end
Controller
#app/controllers/project_applications_controller.rb
def new
#projectapplication = ProjectApplication.build
#project = #projectapplication.project
end
def create
end
private
def application_params
params.require(:project_application).permit(:application, :attributes, :here, answer_attributes: [:content])
end
Views
#app/views/project_applications/new.html.erb
<%= form_for #projectapplication do |f| %>
<%= f.text_field :application_fields %>
<% #project.questions.each do |question| %>
<%= f.fields_for :answers, question do |answer| %>
<%= answer.hidden_field :question_id, question.id %>
<%= answer.text_field :content, placeholder: question.content %>
<% end %>
<% end %>
<% end %>
Process
This works by creating a new project_application, sending answers directly to the answer model. Because answers are directly associated with questions, and projectsthrough questions, you should be able to get it working without passing any more data
This might not work outright, but I'm sure with some tweaking it will deliver the desired result

no method error in one to many relationship

models/user.rb
class User < ActiveRecord::Base
has_many :clubs, :dependent => :destroy
has_many :announcements, :dependent => :destroy
end
models/announcement.rb
class Announcement < ActiveRecord::Base
belongs_to :user
belongs_to :club
end
models/club.rb
class Club < ActiveRecord::Base
belongs_to :user
has_many :announcements, :dependent => :destroy
end
controllers/announcements/announcements_controller.rb
def index
#announcements = Announcement.find(:all, :include => [:user, :club])
end
Problem:
When i type this code,
views/announcements/index.html.erb
<% #announcements.each do |announcement| %>
<%= announcement.user.username %>
<% end %>
I get this error:
NoMethodError in Announcements#index
undefined method `username' for nil:NilClass
when i change code to this, it works.
<% #announcements.each do |announcement| %>
<%= announcement.club.user.username %>
<% end %>
Why is the first code is not working? What is the difference between these codes. Thanks.
It looks like you've got an announcement which has no user set, but does have a club which in turn does have a user. Perhaps consider adding a validation to require that the :user_id column on announcement is present.

Resources