has_many_through new association for exisitng records

In a has_many_through association where both records exist how do I create a new association
In this case Person has_many_pictures through person_picture and I’m trying to add an existing picture to an existing person like this:

@person.pictures << @picture

But this creates a new picture instead of adding the association

I can't duplicate this finding here. Here's the console log:

2.4.2 :001 > p = Person.new name: 'Walter'
=> #<Person id: nil, name: "Walter", created_at: nil, updated_at: nil>
2.4.2 :002 > p.save
   (0.2ms) begin transaction
  SQL (2.8ms) INSERT INTO "people" ("name", "created_at", "updated_at") VALUES (?, ?, ?) [["name", "Walter"], ["created_at", "2018-02-04 21:47:31.000973"], ["updated_at", "2018-02-04 21:47:31.000973"]]
   (1.3ms) commit transaction
=> true
2.4.2 :003 > c = Picture.new file: 'some file'
=> #<Picture id: nil, file: "some file", created_at: nil, updated_at: nil>
2.4.2 :004 > c.save
   (0.2ms) begin transaction
  SQL (2.2ms) INSERT INTO "pictures" ("file", "created_at", "updated_at") VALUES (?, ?, ?) [["file", "some file"], ["created_at", "2018-02-04 21:47:54.717609"], ["updated_at", "2018-02-04 21:47:54.717609"]]
   (1.4ms) commit transaction
=> true
2.4.2 :005 > p.pictures << c
   (0.1ms) begin transaction
  SQL (0.6ms) INSERT INTO "person_pictures" ("person_id", "picture_id", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["person_id", 1], ["picture_id", 1], ["created_at", "2018-02-04 21:47:58.847298"], ["updated_at", "2018-02-04 21:47:58.847298"]]
   (1.1ms) commit transaction
  Picture Load (0.4ms) SELECT "pictures".* FROM "pictures" INNER JOIN "person_pictures" ON "pictures"."id" = "person_pictures"."picture_id" WHERE "person_pictures"."person_id" = ? LIMIT ? [["person_id", 1], ["LIMIT", 11]]
=> #<ActiveRecord::Associations::CollectionProxy [#<Picture id: 1, file: "some file", created_at: "2018-02-04 21:47:54", updated_at: "2018-02-04 21:47:54">]>
2.4.2 :006 >

Here's the models:

class Picture < ApplicationRecord
  has_many :person_pictures
  has_many :people, through: :person_pictures
end

class Person < ApplicationRecord
  has_many :person_pictures
  has_many :pictures, through: :person_pictures
end

class PersonPicture < ApplicationRecord
  belongs_to :person
  belongs_to :picture
end

Whatever is happening on your app is not clear, but you can see that after you save the person and the picture, when you add that saved picture to the saved person's 'pictures' collection, the only record that gets created is a person_picture. Now if either the person or the picture was in the "new" state, that is to say, not saved yet, then I could imagine that it would be saved by ActiveRecord first in order to allow the person_picture record to be saved. Both IDs have to be known before the join object can be saved.

Walter

Even more interesting (to me, anyway) is what happens if you create (but don't save) any of these objects:

2.4.2 :006 > p = Person.new name: 'Walter'
=> #<Person id: nil, name: "Walter", created_at: nil, updated_at: nil>
2.4.2 :007 > c = Picture.new file: 'some file'
=> #<Picture id: nil, file: "some file", created_at: nil, updated_at: nil>
2.4.2 :008 > p.pictures << c
=> #<ActiveRecord::Associations::CollectionProxy [#<Picture id: nil, file: "some file", created_at: nil, updated_at: nil>]>
2.4.2 :009 > p.save
   (1.9ms) begin transaction
  SQL (23.1ms) INSERT INTO "people" ("name", "created_at", "updated_at") VALUES (?, ?, ?) [["name", "Walter"], ["created_at", "2018-02-04 23:23:15.342294"], ["updated_at", "2018-02-04 23:23:15.342294"]]
  SQL (0.3ms) INSERT INTO "pictures" ("file", "created_at", "updated_at") VALUES (?, ?, ?) [["file", "some file"], ["created_at", "2018-02-04 23:23:15.374862"], ["updated_at", "2018-02-04 23:23:15.374862"]]
  SQL (0.4ms) INSERT INTO "person_pictures" ("person_id", "picture_id", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["person_id", 2], ["picture_id", 2], ["created_at", "2018-02-04 23:23:15.376805"], ["updated_at", "2018-02-04 23:23:15.376805"]]
   (1.3ms) commit transaction
=> true
2.4.2 :010 >

When you save one of them, all three are saved in order to preserve the entire set of relationships.

Walter

Yea that is interesting, thanks for posting that

So if @person.pictures << @picture adds a picture to a person, how do you remove a picture from a person?

You (effectively) delete the intermediate object. You could try one of two things:

@person.pictures -= [@picture]

If all the items are persisted, then that should delete the join object immediately. (Just tested, it did work here.)

@person.pictures = @person.pictures.to_a.reject{ |p| p == @picture }

(Long-hand way to do the same thing)

Now if you have a UI around this, what you would do is build an array of checkboxes with the picture IDs in them, and then use the helper method @person.picture_ids=(array of ids) that the association built for you. You don't need to do any of the above long-hand.

So in your controller, you would add picture_ids: [] to the end of your list of allowed attributes in the strong parameters. Next, you would create a checkbox for each attached image in your form:

<%- @person.pictures.each do |picture| %>
<%= check_box_tag 'person[picture_ids][]', picture.id, true %>
<%= image_tag picture.file_url %> (just guessing how your internals look, do something here to show a thumbnail)
<%- end %>

And that should do the whole thing for you.

Walter

Yea, that’s pretty good thanks What about the gem that let you use images in select lists I would think a lot of images is too many for a select list

I need to select from all pictures and if the picture’s already associated with the person it would already be checked then I guess, something like that

I dunno why this doesn't produce any output

    <% @pictures=Picture.all %>

Model.all has been deprecated since rails 2.3.8. Don't use it.

    <% @pictures.all do |picture| %>

this should be using each, not all

Colin

I can use <% if picture.person_id == @person.id %> ? This is a has_many_through relationship Person has_many :pictures, through: :person_pictures and also Picture has_many :people, through: :person_pictures What’s the right syntax to test if the picture is already associated to the person when iterating through pictures Also if I can’t use Model.all in a routine I can still use it to get @pictures=Picture.all right?

I dunno why this doesn't produce any output

    <% @pictures=Picture.all %>

Model.all has been deprecated since rails 2.3.8. Don't use it.

    <% @pictures.all do |picture| %>

this should be using each, not all

Colin

<% if @person %>
<% if picture.person_id == @person.id %>
<%= check_box_tag 'person[picture_ids][]', picture.id, true %>
<% else %>
   <%= check_box_tag 'person[picture_ids][]', picture.id %>
<% end %>
<%= image_tag picture.name.thumb %>
<% end %>
    <% end %>

I can use <% if picture.person_id == @person.id %> ? This is a
has_many_through relationship Person has_many :pictures, through:
:person_pictures and also Picture has_many :people, through:
:person_pictures What's the right syntax to test if the picture is already
associated to the person when iterating through pictures Also if I can't
use Model.all in a routine I can still use it to get @pictures=Picture.all
right?

Model.all is deprecated, don't use it.

If Picture has_many people then you can't use picture.person_id as one
picture is associated with many people. so @picture.people is (effectively)
an array of people. You will have determine whether the array includes that
person.

Colin

Thanks, yea that’s what I meant How do I test for the inclusion of picture in @person.pictures If I can’t use Model.all how do I select all pictures from the pictures table?

I dunno why this doesn't produce any output

    <% @pictures=Picture.all %>

Model.all has been deprecated since rails 2.3.8. Don't use it.

    <% @pictures.all do |picture| %>

this should be using each, not all

Colin

    <% if @person %>
      <% if picture.person_id == @person.id %>
        <%= check_box_tag 'person[picture_ids][]', picture.id, true %>
      <% else %>
         <%= check_box_tag 'person[picture_ids][]', picture.id %>
      <% end %>
      <%= image_tag picture.name.thumb %>
    <% end %>
    <% end %>

I can use <% if picture.person_id == @person.id %> ? This is a has_many_through relationship Person has_many :pictures, through: :person_pictures and also Picture has_many :people, through: :person_pictures What's the right syntax to test if the picture is already associated to the person when iterating through pictures Also if I can't use Model.all in a routine I can still use it to get @pictures=Picture.all right?

Model.all is deprecated, don't use it.

If Picture has_many people then you can't use picture.person_id as one picture is associated with many people. so @picture.people is (effectively) an array of people. You will have determine whether the array includes that person.

Colin

--
You received this message because you are subscribed to the Google Groups "Ruby on Rails: Talk" group.
To unsubscribe from this group and stop receiving emails from it, send an email to rubyonrails-ta...@googlegroups.com.
To post to this group, send email to rubyonra...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/rubyonrails-talk/8d160a04-6951-4159-bfa7-8eee046cb3c4%40googlegroups.com.

For more options, visit https://groups.google.com/d/optout.

Thanks, yea that's what I meant How do I test for the inclusion of picture in @person.pictures If I can't use Model.all how do I select all pictures from the pictures table?

I would start by setting up @pictures in the controller, not the view.

@pictures = Picture.order :name

Then in the view, you could use that to create your field of checkboxes:

<%- @pictures.each do |picture| %>

Use the presence of that picture's ID in the @person's picture_ids array to set or not set the checked attribute
<%= check_box_tag 'person[picture_ids][]', picture.id, @person.picture_ids.include?(picture.id) %>
<%= do_something_to_show_a_thumbnail_here %>

<%- end %>

What you will end up doing is mutating the picture_ids array in this form. As long as you have allowed that (and defined it as requiring an array) in your whitelist, then the association will have built the necessary accessor methods and this will all just work.

Walter

Since I’m not creating a new picture, only adding existing pictures to a person I put an update action in the people controller but it tries to create a new picture and fails with validation :name can’t be blank

def add_pictures_update

if (params[:person][:person_id])

@person=Person.find(params[:person][:person_id])

@pictures = Picture.find(params[:person][:picture_ids])

@person.pictures << @pictures

respond_to do |format|

if @person.update

format.html { redirect_to pictures_path(person_id: params[:picture][:person_id]), notice: ‘Person picture updated.’ }

format.json { render :show, status: :created, location: @picture }

I was having trouble hiding and showing elements on the form so I created a separate action in the pictures controller to handle the submission of the form that selects pictures to add to the person from existing pictures in the database It attempts to create a new picture I guess because validation fails :name cannot be blank

def add_from_pictures_create
           if (params[:picture][:person_id])
               @person=Person.find(params[:picture][:person_id])
               @pictures = Picture.find(params[:person][:picture_ids])

# these are now new instances -- with different object IDs -- than the ones you want to compare them with

               @person.pictures << @pictures

# assigning these in this way means that you are adding an array to an array. Trouble is, you are adding duplicate instances in the second array to an array which may already contain the ones you want to add.

               respond_to do |format|
                 if @person.save
                            format.html { redirect_to pictures_path(person_id: params[:picture][:person_id]), notice: 'Person picture updated.' }
                            format.json { render :show, status: :created, location: @picture }

I would avoid this entire approach. Think about this in the abstract: you are editing and updating the @person, manipulating the picture_ids attribute on that instance. Your form is built on the person. This form should be submitted to the PersonController#update method. If you have (as I have said many times in this thread) whitelisted the picture_ids attribute, then simply calling @person.save will persist that attribute -- it is already going to be in the person_params strong parameters hash.

Here's a clue: when you find yourself writing something like this in a controller:

@person=Person.find(params[:picture][:person_id])

...just walk away and have a think about your life.

Walter

I created an update action in the persons controller Valitadation was failing with :name can’t be blank so I assumed @person.save was trying to create a new picture That’s why I moved the action from the pictures controller to the persons controller and changed the action to @person.update instead of @person.save Did you already understand that?

You wanted me to use patch or post to achieve @person.pictures << @pictures and then I would wanna use if @person.save or if @person.update Please clarify that Thanks in advance I took out the stuff left over from when this routine was in the pictures controller and now rely on the callback set_person

That validation was probably on the person, not the picture, unless you added validates_associated to the Person class.

Look at this: https://github.com/walterdavis/fugee/blob/master/app/controllers/people_controller.rb#L74

and this:

https://github.com/walterdavis/fugee/blob/master/app/views/people/_form.html.erb#L25

The rest is scaffolded, there's nothing mysterious here.

Clone this to your machine, run it in rails server.

Go to localhost:3000/pictures and add some pictures (just file names).

Go to localhost:3000/people, and add some people.

See how you can choose pictures for each person? See how the association is saved and updated? Watch in the console as the record is saved or updated from the web.

Walter

I’ll do that today thanks In the meanwhile, the validtion’s in picture.rb

Do I have to create routes like "get ‘/people/:id/addresses’ => ‘addresses#index’, as: ‘person_addresses’ I was thinking maybe rails already creates those routes from the associations and if I make them explicit maybe I’ll mess up the routes