0

I'm using Rails 5.0. I've set up an app where each profile has many locations. I'm having trouble getting the locations to actually save to the db. Each user should be able to save multiple zip codes to their profile. I've associated profile and locations and zip_codes is a field in the locations table. My code is below.

A little more context - I want each user/profile to be associated to many zip codes. The goal is to allow users to search for profiles based on a zip code.

UPDATED CODE I've updated my code as suggested here - How to save a nested resource in ActiveRecord using a single form (Ruby on Rails 5)

I'm still having the issues after updating the Profile.rb model to include accepts_nested_attributes_for :locations I'm still seeing the Unpermitted paramter: location as shown in the console log below.

schema.rb

   create_table "locations", force: :cascade do |t|
    t.integer "user_id"
    t.string  "zip_codes"
    t.integer "profile_id"
    t.index ["profile_id"], name: "index_locations_on_profile_id"
    t.index ["user_id"], name: "index_locations_on_user_id"
  end

  create_table "profiles", force: :cascade do |t|
    t.integer  "user_id"
    t.string   "first_name"
    t.string   "last_name"
    t.string   "services_offered"
    t.string   "phone_number"
    t.string   "contact_email"
    t.string   "description"
    t.datetime "created_at",          null: false
    t.datetime "updated_at",          null: false
    t.string   "avatar_file_name"
    t.string   "avatar_content_type"
    t.integer  "avatar_file_size"
    t.datetime "avatar_updated_at"
    t.string   "business_name"
    t.string   "short_term"
    t.string   "long_term"
  end

  create_table "users", force: :cascade do |t|
    t.string   "email",                  default: "", null: false
    t.string   "encrypted_password",     default: "", null: false
    t.string   "reset_password_token"
    t.datetime "reset_password_sent_at"
    t.datetime "remember_created_at"
    t.integer  "sign_in_count",          default: 0,  null: false
    t.datetime "current_sign_in_at"
    t.datetime "last_sign_in_at"
    t.string   "current_sign_in_ip"
    t.string   "last_sign_in_ip"
    t.datetime "created_at",                          null: false
    t.datetime "updated_at",                          null: false
    t.integer  "plan_id"
    t.string   "stripe_customer_token"
    t.index ["email"], name: "index_users_on_email", unique: true
    t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
  end

user.rb

class User < ApplicationRecord
  
  belongs_to :plan
  has_one :profile 
  has_many :locations, through: :profile
  accepts_nested_attributes_for :locations

profile.rb

class Profile < ActiveRecord::Base
   belongs_to :user
   has_many :locations, inverse_of: :profile
   accepts_nested_attributes_for :locations
end

location.rb

class Location < ActiveRecord::Base
    belongs_to :user, optional: true
    belongs_to :profile, optional: true
end

profiles_controller.rb

class ProfilesController < ApplicationController

  #POST to /users/:user_id/profile
   def create
       # Ensure we have the user who is filling out the form
      @user = User.find( params[:user_id] ) 
      # Create profile linked to this specific user
      @profile = @user.build_profile( profile_params )
      if @profile.save
          flash[:success] = "Profile saved!"
          redirect_to user_path( params[:user_id] )
      else
          render action: :new
      end
   end

   #PUT to /users/:user_id/profile
   def update
       # Retrieve user from the database
       @user = User.find( params[:user_id] )
       # Retrieve that user's profile
       @profile = @user.profile
       # Mass assign edited profile attributes and save (update)
       if @profile.update_attributes(profile_params)
           flash[:success] = "Profile updated!"
           # Redirect user to profile page
           redirect_to user_path(id: params[:user_id])
       else
           render action: :edit
       end
   end

 private
    def profile_params
    params.require(:profile).permit(:first_name, :last_name, :avatar, :job_title, :phone_number, :contact_email, :description, :services_offered, :long_term, :short_term, locations_attributes: [:id, :zip_codes])
   end
end

And here is the view where I'm trying to insert the zip_codes field and allow users to save many to their profile. The field shows and I can successfully submit, but the zip codes won't show when I go back to edit it. I'm also not sure they're saving.

_form.html.erb

<%= form_for @profile, url: user_profile_path, :html => { :multipart => true } do |f| %>
<div class="form-group">
    <%= f.label :first_name %>
    <%= f.text_field :first_name, class: 'form-control' %>
  </div>
  <div class="form-group">
    <%= f.label :last_name %>
    <%= f.text_field :last_name, class: 'form-control' %>
  </div>
  </div>
    <div class="form-group">
    <%= f.fields_for :locations do |f| %>
    <%= f.label :zip_codes, "Zip codes served" %></br>
    <%= f.text_field :zip_codes, 'data-role'=>'tagsinput' %>
    <% end %>
  </div>
<% end %>

UPDATE* Here's the console PATCH request. I edited a few other fields, so it's clear that nothing is running to update the zip_codes field.

Started PATCH "/users/1/profile" for 24.9.150.43 at 2020-11-23 23:35:53 +0000
Cannot render console from 24.9.150.43! Allowed networks: 127.0.0.1, ::1, 127.0.0.0/127.255.255.255
Processing by ProfilesController#update as HTML
  Parameters: {"utf8"=>"✓", "authenticity_token"=>"QPHsRWYDY5hxHr4zEL/XW8h2qzDqvhqIPvU/mBlc4exOyqHSxD9A9vSzShNF+afllkDFejqsSP0DiVwGa/9jsQ==", "profile"=>{"first_name"=>"Person", "last_name"=>"Test", "long_term"=>"true", "short_term"=>"true", "location"=>{"zip_codes"=>"80212"}, "phone_number"=>"507-222-2222", "contact_email"=>"[email protected]", "description"=>"Test account with test description."}, "commit"=>"Update Profile", "user_id"=>"1"}
  User Load (0.2ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? ORDER BY "users"."id" ASC LIMIT ?  [["id", 1], ["LIMIT", 1]]
  User Load (0.1ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
  CACHE (0.0ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
  Profile Load (0.1ms)  SELECT  "profiles".* FROM "profiles" WHERE "profiles"."user_id" = ? LIMIT ?  [["user_id", 1], ["LIMIT", 1]]
Unpermitted parameter: location
   (0.1ms)  begin transaction
  SQL (7.2ms)  UPDATE "profiles" SET "first_name" = ?, "short_term" = ?, "updated_at" = ? WHERE "profiles"."id" = ?  [["first_name", "Person"], ["short_term", "true"], ["updated_at", 2020-11-23 23:35:53 UTC], ["id", 1]]
   (7.8ms)  commit transaction
Redirected to https://a434446a3d264614901296378175dad9.vfs.cloud9.us-west-2.amazonaws.com/users/1
Completed 302 Found in 28ms (ActiveRecord: 15.3ms)
17
  • Does this answer your question? How to save a nested resource in ActiveRecord using a single form (Ruby on Rails 5) Commented Nov 23, 2020 at 22:33
  • Can you post the webserver console output when you save the profile? Commented Nov 23, 2020 at 23:05
  • Also what are your User relations? It looks like a User has many profiles and the User has many locations through profiles? Something seems off about your relations. And there probably should be some accepts_nested_attributes_for in profile. Commented Nov 23, 2020 at 23:11
  • Thank you. I think that did answer the question, so I've updated the profile.rb file to be has_many :locations, inverse_of: :profile accepts_nested_attributes_for :locations and the view has </div> <div class="form-group"> <%= f.fields_for :locations do |locations_form| %> <%= locations_form.label :zip_codes, "Zip codes served" %></br> <%= locations_form.text_field :zip_codes, class: 'form-control' %> <% end %> </div> The only issue is that the field no longer shows on the frontend. Commented Nov 23, 2020 at 23:24
  • I've also updated the permit params in the controller in the format you linked to - locations_attributes: [:id, :zip_codes] Commented Nov 23, 2020 at 23:26

1 Answer 1

0

OK, since you want to be able to search for profiles via zip code you will want to store the zip codes in the location table. But if you have 100 users that list 98057 as their zip code you don't want 100 unique locations. You want a location with zip 98057, and a join table with an entry of profile_id and location_id.

So Profile model would look like:

class Profile < ActiveRecord::Base
  belongs_to: :user
  has_many: :profile_locations inverse_of: :profile
  has_many: locations through: profile_locations

  accepts_nested_attributes_for: :profile_location
end

And Location model:

class Location < ActiveRecord::Base
  has_many: profile_locations
  has_many: profiles through: profile_locations
end

Add a ProfileLocation model:

class ProfileLocation < ActiveRecord::Base
  belongs_to: :profile
  belongs_to: :location
  validates_presence_of :profile
  validates_presence_of :location

  accepts_nested_attributes_for :location

end

The locations table should be:

create_table "locations", force: :cascade do |t|
    t.string  "zip_code"  #note SINGULAR
end

And the profile_locations table would be:

create_table :profile_locations do |t|
  t.belongs_to :location
  t.belongs_to :profile
  t.timestamps
end

That's the easy part. Now you have to get the zip codes in and out of the DB using your form:

<div class="form-group">
  <%= f.label :zip_codes, "Zip codes served" %></br>
  <%= f.text_field :zip_codes, value: @profile.locations.pluck(:zip_code).join(' ,'),'data-role'=>'tagsinput' %>
</div>

We use value: @profile.locations.pluck(:zip_code).join(' ,') to fill in the edit version of the form with the current zip codes in a text field. When it is in the new form it will be nil.

So you will get a param of

profile: {zip_codes: '99019, 98057, 06850'}

You need to convert those into an array so add a method to Profiles controller:

class ProfilesController < ApplicationController
  before_action: :zip_array only:[:create, :update]
   ....

And then make a private method like:

def zip_array
  profile_params[:zip_codes] = profile_params[:zip_codes].scan(/\d{5}/)
  #replaces the string '99019, 98057, 06850' with the array ['99019', '98057', '06850']
end

In your create and update methods you want to build the associated zip code entries. But in update you want to get rid of any that already exist an update in case the person is removing some zip codes and adding others.

So in the Create action:

def create
   # Ensure we have the user who is filling out the form
  @user = User.find( params[:user_id] ) 
  # Create profile linked to this specific user
  @profile = @user.build_profile( profile_params )
  profile_params[:zip_codes].each do |zip|
    @profile.locations.build(zip_code: zip)
  end
  if @profile.save
      flash[:success] = "Profile saved!"
      redirect_to user_path( params[:user_id] )
  else
      render action: :new
  end
end

And in the Update action:

def update
  # Retrieve user from the database
  @user = User.find( params[:user_id] )
  @profile = user.profile
  # we wrap this in a rescue block in case the transaction fails
  begin
    Profile.transaction do 
      @profile.profile_locations.destroy_all  #get rid of all of them
      profile_params[:zip_codes].each do |zip|
       @profile.locations.build(zip_code: zip) #build the new locations
      end
      @profile.update_attributes(profile_params) #saving the profile saves the locations we built
      flash[:success] = "Profile updated!"
      redirect_to user_path(id: params[:user_id])
    end  
  rescue  #if the transaction fails it will roll back the destroy_all
    render action: :edit
  end
end
Sign up to request clarification or add additional context in comments.

9 Comments

You are using some odd conventions in your app design, so the normal way that nested forms are used doesn't really fit. If you had just one zip code per profile, it would be far simpler. Also if you were nesting a resource that was just a has_many and belongs_to relationship you'd use zipcode_ids in the nested attributes.
Thank you! I appreciate your help and it makes sense to me. I didn't think of having the join table. I made the updates and am seeing a undefined method `scan' for nil:NilClass error. It seems like the scan method is causing some issues. Currently debugging it.
If the zipcode field is blank, then .scan gets called on nil. You can add a validation that requires it to not be blank. Or use profile_params[:zip_codes].try(:scan, (/\d{5}/))
There are a lot of situations in Rails where you might get a nil value and that's OK, but many methods don't play well with that. So the came up with .try(:method, arguments) so you don't have to constantly have if or unless statements for when the value is nil.
I'm actually sending it with a value and it's still returning that error. When I make the .try change you suggest I now get undefined method `each' for nil:NilClass. I'm not sure why it's still sending nil I can see these parameters getting passed in the request "locations"=>{"zip_codes"=>"12345"},
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.