Forms for has_one but not always - ruby-on-rails

I have two models: Personand User. A person can have a single user or no user at all, but every user have to be belong to a person.
I have set both models like so:
Person (has a has_user attribute):
has_one :user, dependent: :destroy
accepts_nested_attributes_for :user
User (has person_id attribute):
belongs_to :person
validates_presence_of :login, :password
validates_uniqueness_of :login
In the Person form I have a nested form_for that only shows up if I check a check box that also set the has_user attribute to true on the Person's form part.
My issue is that whenever I submit a person that has no user, it still tries to validate the user. How can I make it work?
UPDATE:
In my view:
<div class="form-group">
<%= person_form.label :has_user, :class => 'inline-checkbox' do %>
Possui usuário
<%= person_form.check_box :has_user, {id: "hasUser", checked: #person.has_user} %>
<% end %>
</div>
</div>
<div id="userPart" class="findMe"
<% if #person.user.id.blank? %> style="display:none;"
<% end %>>
<h2> User: </h2>
<div class="container">
<%= person_form.fields_for :user do |user_form| %>
<%= render partial: 'users/campos_user', locals: {form: user_form} %>
<% end %>
</div>
</div>
In my Person.js:
jQuery ->
$(document).ready ->
$(document).on 'turbolinks:load', #Added line
console.log("Turbolinks ready.")
$("#hasUser").change ->
$("#userPart").toggle();
return
return
return
return
In my Person Controller:
def new
#person= Contato.new
#person.ativo = true
#page_title = 'Novo person'
#person.build_user
end
def create
#person = Person.new(person_params)
respond_to do |format|
if #person.save
flash[:notice] = 'Contato foi criado com sucesso.'
if #person.has_user == true
format.html {redirect_to controller: :users, action: :new, person_id: #person.id}
else
format.html {redirect_to #person}
end
format.json {render :show, status: :created, location: #person}
else
flash[:warn] = "Erro ao criar contato."
format.html {render :new}
format.json {render json: #person.errors, status: :unprocessable_entity}
end
end
end
Person's params:
def person_params
params.require(:person).permit(:id, :company_id,
:active, :name,
:cargo, :celular,
:email, :nascimento,
:observacoes, :mensagem_instantanea,
:tipo_msg_inst, :possui_usuario,
user_attributes: [:login, :password, :permits, :id, :person_id, :_destroy])
end

You can reject submitting user attributes by using reject_if option
accepts_nested_attributes_for :user, :reject_if => :no_user_connected
def no_user_connected(attributes)
attributes[:user_attributes][:login].blank?
end
This should discard the user part of the form (all user attributes). Please note that I dunno how your form or controller looks like, so you might need to build the condition differently.
You can also make the validations on user conditional. But overall the best way to avoid convoluted validation problems is to use some kind of form objects where you encapsulate the validations process. https://thoughtbot.com/blog/activemodel-form-objects

Related

Rails: How to save attribute for parent User (current_user) when submitting form to create child object?

I have a User model (using Devise) and a Listing model. User has_many :listings and Listing belongs_to :user.
The user has email, password and phone number attributes, but when creating an account, only email and password are required. Phone number can be left blank.
When a logged-in user creates a Listing, I'd like to provide the option for the current user to include his phone number using the same form. The expectation is to store the phone number of the user who is creating the listing.
I've created forms to accept nested attributes for child objects before, but not for parent object like this case. I haven't figured out how to get the form to do this, and whether I need to change something in the controller or models. Any help is appreaciated.
User model
class User < ApplicationRecord
has_many :listings, dependent: :destroy
end
Listing model
class Listing < ApplicationRecord
belongs_to :user
end
Listings controller
def create
#listing = current_user.listings.build(listing_params)
respond_to do |format|
if #listing.save
format.html { redirect_to listing_url(#listing), notice: "Listing was successfully created." }
format.json { render :show, status: :created, location: #listing }
else
...
end
end
end
New Listing Form
<%= form_with model: #listing do |f| %>
<div class="field">
<%= f.label :title, class: "label required" %>
<div class="control">
<%= f.text_field :title, autofocus: true, autocomplete: "title", required: true, class: "input" %>
</div>
</div>
...
<% end>
Thanks
For save the phone at the user, you can do something like that in the controller:
def create
#listing = current_user.listings.build(listing_params)
user = current_user
user.phone = params[:phone]
respond_to do |format|
if #listing.save && user.save
format.html { redirect_to listing_url(#listing), notice: "Listing was successfully created." }
format.json { render :show, status: :created, location: #listing }
else
...
end
end
end
My solution is a little overcomplicated, but it uses the standard nested attributes and strong params. It will also prepopulate the phone input if the data is available.
Enable nested_attributes on Listing.user. Limit it to update_only since you don't want to be able to create a user in this way.
class Listing < ApplicationRecord
belongs_to :user
accepts_nested_attributes_for :user, update_only: true
end
Add the following nested form to the Listing form partial. Note that include_id is set to false so that the User's ID is not exposed in the form.
<%= f.fields_for :user, include_id: false do |uf| %>
<div class="field">
<%= uf.label :phone, class: "label required" %>
<div class="control">
<%= uf.text_field :phone, autofocus: true,
autocomplete: "title", required: true, class: "input" %>
</div>
</div>
<% end %>
Permit the nested user params in the controller:
def listing_params
params.fetch(:listing, {}).permit(:title, user_attributes: [:phone])
end
Initialize the Listing with a user in the new action of the controller.
def new
#listing = Listing.new(user: #current_user)
end
Finally, in your create action, you'll need to merge the user id into the params, since it wasn't included with the form submission. The user id has to be set both in the nested attributes, and on the new listing record.
def create
#listing = Listing.new(
listing_params.to_h.deep_merge(
user_id: #current_user.id,
user_attributes: { id: #current_user.id }
))
...
end

Form associations: Having trouble with controller saving information to 2 models simultaneously

For my project I am trying to build a dashboard whereby an Agent can view submissions posted by a user and add a Status & Notes to each submission in order to log their own personal activity i.e they would not be changing the actual record, just leaving private notes against it. In order to do this I have created a join table with both Agent id and Submission id as well as Status and Notes columns.
I have managed to create an index view that shows submissions data with 2 form fields at the end of each line from my join table which are called Status and Notes... the problem is when I update these fields they do not get saved to my jointable.
Form on index view
<%= form_with(model: submission, local: true) do |form| %>
<% form.fields_for :agent_activities do |act| %>
<td> <div class="field">
<%= act.text_field :Status %>
</div>
</td>
<td> <div class="field">
<%= act.text_field :Notes %>
</div>
</td>
<td>
<div class="actions">
<%= form.submit %>
</div>
</td>
<% end %>
<% end %>
Model associations in rb files
class Submission < ApplicationRecord
belongs_to :user, :optional => true
belongs_to :location, :optional => true
has_many :agent_activities
end
class AgentActivity < ApplicationRecord
belongs_to :submission, :optional => true #has submission_id
foreign key in table
belongs_to :agent, :optional => true #has agent_id foreign key in
table
end
Controller:
class SubmissionsController < ApplicationController
before_action :set_submission, only: [:show, :edit, :update, :destroy]
def index
#submissions = Submission.where(:user_id => current_user.id)
end
def show
end
def new
#submission = Submission.new
end
def edit
end
# POST /submissions
# POST /submissions.json
def create
#submission = Submission.new(submission_params.merge(user_id: current_user.id))
respond_to do |format|
if #submission.save
# Tell the UserMailer to send a welcome email after save
NewSubmissionMailer.submission_email(#submission).deliver_now
NewSubmissionMailer.matching_agents_email(#submission).deliver_now
format.html { redirect_to #submission, notice: 'Submission was successfully created.' }
format.json { render :show, status: :created, location: #submission }
else
format.html { render :new }
format.json { render json: #submission.errors, status: :unprocessable_entity }
end
end
end
# PATCH/PUT /submissions/1
# PATCH/PUT /submissions/1.json
def update
respond_to do |format|
if #submission.update(submission_params)
format.html { redirect_to #submission, notice: 'Submission was successfully updated.' }
format.json { render :show, status: :ok, location: #submission }
else
format.html { render :edit }
format.json { render json: #submission.errors, status: :unprocessable_entity }
end
end
end
# DELETE /submissions/1
# DELETE /submissions/1.json
def destroy
#submission.destroy
respond_to do |format|
format.html { redirect_to submissions_url, notice: 'Submission was successfully destroyed.' }
format.json { head :no_content }
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_submission
#submission = Submission.find(params[:id])
end
# Never trust parameters from the scary internet, only allow the white list through.
def submission_params
params.require(:submission).permit(:First_Name, :Last_Name, :Phone, :Email, :Desired_Location, :number_of_beds, :number_of_occupants, :Rent_price_per_month_gbp, :Max_move_in_date, :Tenant_Occupation, :Contact_me_on, :Furnished, :Current_Address, :Property_Requirements)
end
end
Not sure what im missing here :/
UPDATE BASED OFF #TOM ANSWER
New controller params:
def submission_params
params.require(:submission).permit(:First_Name, :Last_Name, :Phone, :Email, :Desired_Location, :number_of_beds, :number_of_occupants, :Rent_price_per_month_gbp, :Max_move_in_date, :Tenant_Occupation, :Contact_me_on, :Furnished, :Current_Address, :Property_Requirements, agent_activities_attributes: [:id, :Status, :Notes, :_destroy])
end
end
New Submission Model rb:
class Submission < ApplicationRecord
belongs_to :user, :optional => true
belongs_to :location, :optional => true
has_many :agent_activities
accepts_nested_attributes_for :agent_activities
end
Index.html.erb
<%= form_with(model: submission, local: true) do |form| %>
<% form.fields_for :agent_activities, #submission.agent_activities.build do |act| %>
<td> <div class="field">
<%= act.text_field :Status %>
</div>
</td>
<td> <div class="field">
<%= act.text_field :Notes %>
</div>
</td>
<td>
<div class="actions">
<%= form.submit %>
</div>
</td>
<% end %>
On your Submission model add: accepts_nested_attributes_for :agent_activities (accepts_nested_attributes_for documentation) This will let Rails know that your form is going to be supplying fields for an associated model.
Once that is added Rails will be supplying a key in params agent_activities_attributes in your strong params we can add: .permit(..., agent_activities_attributes: [:id, :Status, :Notes, :_destroy]. The :_destroy key is only needed if you plan on having allow_destroy: true on the nested attribute call.
One side note: Capitalized names (Status, Notes, etc) are normally reserved for constants in Ruby. You may want to look into changing your attribute column names to lowercase.

Nested Rails 4 Form Accessing Array

I have a nested form that has 3 fields repeated 7 times. I want to do a check that if the first field group is empty, then it will stop the process and return an error asking a user to fill in those fields.
I already have a check in that it will drop any empty field groups from the Create action in the controller, that is the "reject_if: => :all_blank" part of the model. However, it deletes the first entry, which I would instead rather run a check on. This code doesn't work, and I don't know where to go from here besides trying to refine the check_attendee_form method. Any help would be appreciated out there.
Here are the related Models:
class Registration < ActiveRecord::Base
# Database Relationships
belongs_to :event
belongs_to :user
has_many :attendees
# Form relationships
accepts_nested_attributes_for :attendees,
:allow_destroy => true,
:reject_if => :all_blank
class Attendee < ActiveRecord::Base
# Database relationships
belongs_to :event
belongs_to :user
delegate :event, :event_id, to: :registration
# Validations
validates :first_name, :last_name, presence: true
Controller Create and New Actions and Methods
def new
#registration = #event.registrations.new
7.times { #registration.attendees.build }
end
def create
#registration = #event.registrations.new(registration_params)
#registration.user = current_user
check_attendee_form
respond_to do |format|
if #registration.save
format.html { redirect_to event_path(#event), notice: 'You are now registered.' }
format.json { render :show, status: :created, location: #registration }
else
format.html { render :new }
format.json { render json: #registration.errors, status: :unprocessable_entity }
end
end
end
def check_registration
#check = Registration.find_by event_id: #event, user_id: current_user
if #check.nil?
#registration = #event.registrations.new
#registration.user = current_user
else
redirect_to event_path(#event),
notice: "You're already registered!"
end
end
def check_attendee_form
#attendees_check = #registration.find_by(attendees_attributes: params[:first_name])
if #attendees_check.first.nil?
render :new, notice: "You need to put in your name at least!"
else
end
end
And finally, the essential form info:
<%= simple_form_for [#event, #registration], :html => { :class => 'form-horizontal' } do |f| %>
<%= render 'shared/errors', object: #registration %>
<%= f.simple_fields_for :attendees, defaults: { input_html: { class: 'form-horizontal' } } do |a| %>
<div>
<%= a.label :first_name %>:
<%= a.text_field :first_name %>
<%= error_span([:first_name]) %>
<%= a.label :last_name %>:
<%= a.text_field :last_name %>
<%= error_span([:last_name]) %>
<%= a.label :fundraising_goal %>:
<%= a.number_field :fundraising_goal, placeholder: 'No Commas' %>
<%= error_span([:fundraising_goal]) %>
Here are the params that are getting submitted:
{"utf8"=>"✓",
"authenticity_token"=>"dNR5QCBFplsAG0wzy87+hzaKuG6h2Mlb6xpmKEM0Kko=",
"registration"=>{"attendees_attributes"=>{"0"=>{"first_name"=>"",
"last_name"=>"",
"fundraising_goal"=>""},
"1"=>{"first_name"=>"",
"last_name"=>"",
"fundraising_goal"=>""},
"2"=>{"first_name"=>"",
"last_name"=>"",
"fundraising_goal"=>""},
"3"=>{"first_name"=>"",
"last_name"=>"",
"fundraising_goal"=>""},
"4"=>{"first_name"=>"",
"last_name"=>"",
"fundraising_goal"=>""},
"5"=>{"first_name"=>"",
"last_name"=>"",
"fundraising_goal"=>""},
"6"=>{"first_name"=>"",
"last_name"=>"",
"fundraising_goal"=>""}}},
"commit"=>"Create Registration",
"event_id"=>"5"}
How do I access the first array object with attendees_attributes of ID [0] in this form submission?
If you want to ensure that at least one Attendee is entered for a registration, I believe you could drop the check_attendee_form method and add the following validation to registration.rb:
validate :check_minimum_attendees
def check_minimum_attendees
errors.add(:attendees, 'must be entered') if attendees.size == 0
end
Your form attributes is nested hash, you can access attendees using id like 0, 1 etc
i.e params[registration][attendees][0]
If I am correct, the problem is with :reject_if condition.
You can use a Proc (or lambda) to 'reject' any unsuitable nested attributes.
:reject_if => lambda { |e| e[:last_name].blank? && (e[:_destroy].blank? || e[:_destroy] == 'false') }
Use validation as #keyzee suggested:
validate :check_attendees
def check_attendees
errors.add(:attendees, 'must be entered') unless attendees.count
attendees.each do |a|
errors.add(:attendees, 'error message here') if condition_here
end
end
Or create custom validator in lib folder (lib/check_attendees_validator.rb):
class CheckAttendeesValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
record.errors[attribute] << 'error_message' if condition_here
end
end
And then use it in a model:
validates :attendees, check_attendees: true

Saving to another Model database, with current Form record ID - Rails

I'm trying to get the text from a text_area field in a form to save to a database in a different Model with the current Model's ID.
Currently, this works but only will save integers. If I put text into the 'Notes' field, then its saves it as a '0'. I suspect this is working correctly but I'm missing a piece to my puzzle. This is because I only want the 'Ticket' to save the note_id because I will have multiple 'Notes' per 'Ticket.
How can I get the Note to save in the Note Model, with an ID, and associate that note_id with this specific ticket?
Form - /app/views/tickets/_form.html.erb
<%= form_for(#ticket) do |f| %>
<% if #ticket.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(#ticket.errors.count, "error") %> prohibited this ticket from being saved:</h2>
<ul>
<% #ticket.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="field">
<%= f.fields_for :notes do |u|%>
<%= u.label :note %>
<%= u.text_area :note, :size => "101x4", :placeholder => "Leave notes here." %>
<% end %>
</div>
<div class="actions">
<%= f.submit %>
</div>
<% end %>
Tickets_controller.rb
class TicketsController < ApplicationController
# GET /tickets
# GET /tickets.json
def index
#tickets = Ticket.all
respond_to do |format|
format.html # index.html.erb
format.json { render json: #tickets }
end
end
# GET /tickets/1
# GET /tickets/1.json
def show
#ticket = Ticket.find(params[:id])
respond_to do |format|
format.html # show.html.erb
format.json { render json: #ticket }
end
end
# GET /tickets/new
# GET /tickets/new.json
def new
#ticket = Ticket.new
#ticket.notes.build
respond_to do |format|
format.html # new.html.erb
format.json { render json: #ticket }
end
end
# GET /tickets/1/edit
def edit
#ticket = Ticket.find(params[:id])
end
# POST /tickets
# POST /tickets.json
def create
#ticket = Ticket.new(params[:ticket])
respond_to do |format|
if #ticket.save
format.html { redirect_to #ticket, notice: 'Ticket was successfully created.' }
format.json { render json: #ticket, status: :created, location: #ticket }
else
format.html { render action: "new" }
format.json { render json: #ticket.errors, status: :unprocessable_entity }
end
end
end
# PUT /tickets/1
# PUT /tickets/1.json
def update
#ticket = Ticket.find(params[:id])
respond_to do |format|
if #ticket.update_attributes(params[:ticket])
format.html { redirect_to #ticket, notice: 'Ticket was successfully updated.' }
format.json { head :no_content }
else
format.html { render action: "edit" }
format.json { render json: #ticket.errors, status: :unprocessable_entity }
end
end
end
# DELETE /tickets/1
# DELETE /tickets/1.json
def destroy
#ticket = Ticket.find(params[:id])
#ticket.destroy
respond_to do |format|
format.html { redirect_to tickets_url }
format.json { head :no_content }
end
end
end
Note.rb
class Note < ActiveRecord::Base
belongs_to :ticket
attr_accessible :note, :ticket_id
end
Ticket.rb
class Ticket < ActiveRecord::Base
attr_accessible :notes_attributes
accepts_nested_attributes_for :notes
end
It is because note_id is an integer type.
Use nested models:
Refer this for Nested Models
Model:
class Ticket < ActiveRecord::Base
has_many :notes
attr_accessible :note_id, :notes_attributes
accepts_nested_attributes_for :notes
end
View:
<%= form_for(#ticket) do |f| %>
<%= f.fields_for :notes do |u|%>
<%= u.label :note %>
<%= u.text_area :note %>
<% end %>
<%= f.submit 'Submit' %>
<% end %>
What you have is a nested association, with Ticket as the "parent". The association is governed by the link from note_id in the Note model to the id (primary key) of the Ticket. What you're presently doing right now is manually manipulating that numeric association. Rails, knowing that the note_id column is supposed to be an integer, is taking the text you're trying to insert and turning it in to a number (zero in this case). You've probably got a bunch of orphaned rows right now because of this.
Ultimately, in order to accomplish what you're trying to do, your form will need to provide fields for that associated model. One way you can handle this is by using the accepts_nested_attributes_for in your Ticket model. Like so:
class Ticket < ActiveRecord::Base
has_many :notes
accepts_nested_attributes_for :notes
end
And in your form, you can easily create a nested form like so:
<%= form_for(#ticket) do |f| %>
<div class="field">
<%= f.fields_for :notes do |f_notes|%>
<%= f_notes.label :note %><br />
<%= f_notes.text_area :note, :size => "101x4", :placeholder => "Please leave notes here."%>
<% end %>
</div>
<% end %>
Edit Almost forgot: Check out this Railscast from Ryan Bates dealing with Nested Attributes
Edit 2 As codeit pointed out, you don't need the attr_accessible :note_id in Ticket. Since you've indicated that a Ticket has many Notes, and that Note belongs to Ticket, the foreign key column will appear in the Note model as ticket_id, which you already have. Having note_id in the ticket model is useless, and also nonsensical since has_many describes a plural relationship (which can't be expressed with a single column).

Rails: Updating Nested attributes - undefined method `to_sym' for nil:NilClass

Edit: Added the update action, and on what line the error occurs
Model:
class Match < ActiveRecord::Base
has_and_belongs_to_many :teams
has_many :match_teams
has_many :teams, :through => :match_teams
accepts_nested_attributes_for :match_teams, :allow_destroy => true
end
Controller:
def new
#match = Match.new
#match_teams = 2.times do
#match.match_teams.build
end
respond_to do |format|
format.html # new.html.erb
format.json { render json: #match }
end
end
def update
#match = Match.find(params[:id])
respond_to do |format|
if #match.update_attributes(params[:match])
format.html { redirect_to #match, notice: 'Match was successfully updated.' }
format.json { head :ok }
else
format.html { render action: "edit" }
format.json { render json: #match.errors, status: :unprocessable_entity }
end
end
end
Nested model:
class MatchTeam < ActiveRecord::Base
belongs_to :match
belongs_to :team
end
Association:
class Team < ActiveRecord::Base
has_and_belongs_to_many :matches
end
View:
<%= form_for(#match) do |f| %>
<%= f.fields_for :match_teams, #match_teams do |builder| %>
<%= builder.collection_select :team_id, Team.all, :id, :name, :include_blank => true %>
<% end %>
<% unless #match.new_record? %>
<div class="field">
<%= f.label :winning_team_id %><br />
<%= f.collection_select :winning_team_id, #match.teams, :id, :representation %>
</div>
<% end %>
<div class="actions">
<%= f.submit %>
</div>
<% end %>
Params:
Processing by MatchesController#update as HTML
Parameters: {"utf8"=>"Ô£ô", "authenticity_token"=>"QIJChzkYOPZ1hxbzTZS8H3AXc7i
BzkKv3Z5daRmlOsQ=", "match"=>{"match_teams_attributes"=>{"0"=>{"team_id"=>"1", "
id"=>""}, "1"=>{"team_id"=>"3", "id"=>""}}, "winning_team_id"=>"3"}, "commit"=>"
Update Match", "id"=>"2"}
Creating a new match with 2 teams work fine, the edit view also shows the correct values, but the update action gives me this error.
undefined method `to_sym' for nil:NilClass
app/controllers/matches_controller.rb:65:in `block in update'
line 65: if #match.update_attributes(params[:match])
I've figured it out. I read that a join table like MatchTeams doesn't need an ID. I'm guessing this is true when not doing any nested forms. I redid my migration removing the exclusion of the id column, and now everything works fine. Don't we all love this stupid errors? :)
Without seeing the offending to_sym in your code, just know that the thing it's attached to has not been defined properly. If this is a variable such as #var.to_sym, you most likely:
Haven't set #var at all
Set it but it's returning nil because there are no matches (e.g. #var = #project.companies.first but #project has no companies tied to it).
You are missing a relevant bit of data in your params. If your to_sym is relying on data submitted through the form, it won't work if the user leaves out the bit of data you're assuming. In this case, you should test first to see if the data was entered before running .to_sym on it.

Resources