Saving an association from a select input

Hey friends! I’m very new to Elixir and Phoenix and this community has been extremely helpful so far. Unfortunately, I am running into some issues with my understanding. I’m hoping someone can suggest some reading or point me in the right direction. I’ve found lots of resources that come close to my issue, but I can’t bridge the gap in my understanding.

I have many Students who belong to a single Session.

student.ex schema example

schema "students" do
  field :first_name, :string
  field :last_name, :string
  belongs_to :session, Session, foreign_key: :bold_session

session.ex schema example

schema "sessions" do
  field :name, :string
  field :active, :boolean, default: false
  has_many(:student, Student)

When creating a Student, I need to assign them to a Session. In order to do that, I query all of the available sessions and pass them into a select in the form.

The query looks like this:

def session_query() do
  session_query = from(s in Session, select: {s.name, s.id})
  Repo.all(session_query)
end

The select looks like this:

<%= inputs_for f, :session, fn sf -> %>
  <%= label sf, :session %>
  <%= select sf, :session, @sessions, prompt: "Choose a session" %>
  <%= error_tag sf, :session %>
<% end %>

Everything renders correctly - However, when I submit the form, the Student is not created.

The student’s session is submitted as:

"session" => %{"session" => "5"}

However, when I inspect the errored changeset in student_controller I get:

session: #Ecto.Changeset<
  action: :insert,
  changes: %{},
  errors: [name: {"can't be blank", [validation: :required]}],
  data: #Bold.Sessions.Session<>,
  valid?: false
>

Obviously my understanding of Elixir and Phoenix is pretty limited. I’m not understanding what Name has to do with it, although Name is a field of Session. All I want to do is create a Student with a Session and eventually list out all students by session. Any thoughts on what I am missing here? Any help, guidance, tough love, etc is greatly appreciated!

Hello !

You’re using inputs_for, which is made to create / update embedded schema or relationship. When you declare belongs_to, session his a direct link to the related schema, but it’s not the field available on the students table. That macro automatically adds the foreign key, which by default would be session_id.

So in your case, you need to remove the inputs_for, and replace with the normal select, pointing to session_id, like this (actually, has stated by @antoine, you have changed the foreign_key to bold_session, so that should be the one to use here ):

<%= label sf, :bold_session %>
<%= select sf, :bold_session, @sessions, prompt: "Choose a session" %>
<%= error_tag sf, :bold_session %>

Normally, it should then just apply the selection to the proper column and create the student with the relationship assigned.

3 Likes

Thanks @shd42, indeed the select is more appropriate, didn’t know this, I have never used forms.

So if you change your view to use the select instead of the input, you will receive an attrs that looks like this

%{
    ...
    "session_id" => 5,
    ...
}

Then you simply have to cast it

  @fields [:firstname, :lastname, :session_id]
  @required_fields [:firstname, :lastname, :session_id]

  def changeset(student, attrs) do
    student
    |> cast(attrs, @fields)
    |> validate_required(@required_fields)
  end

But this is true only if the foreign_key name in your table is named session_id.

Wich seems to not to be the case, because you overrided it:

  belongs_to :session, Session, foreign_key: :bold_session

Any reason for this ? what was the intention ?

If you need this field ne be named bold_session, just change session_id to bold_session in both changeset and your view.

2 Likes

I think the other members have already adressed your concern.

I just want to comment on a little detail.

To follow the usual association naming (of course this is not mandatory ^^) I would suggest to rename the has_many name to a plural form such as students instead of student. I think it is more readable.

Ex:

has_many :things, Thing
has_many :criteria, Criterion
has_one :profile, Profile
1 Like

Thanks for the feedback. I actually created a column called bold_session to hold the ID in the DB. I did this because I didn’t know the convention. I think I’ll go back and change the structure, as convention makes more sense and I don’t want to go down the rabbit hole of overriding everything.

Also, I was trying to cast_assoc on this, so thanks for the callout!

Completely agree - It felt wrong when I was typing out the question. Thanks for the feedback!

1 Like

This makes a lot more sense. I wasn’t understanding exactly how inputs_for and the associations were working. Thanks for the explanation. It really helped clear things up for me.

1 Like