Phoenix: changeset does not take into account changes on foreign keys - phoenix-framework

I am trying to update values for a record in the console ies -S mix .
iex> video = Repo.one(from v in Video, limit: 1)
%Rumbl.Video{...}
if I change the title of the video, everything seems to be working correctly.
iex> changeset = Video.changeset(video, %{title: "some title"})
#Ecto.Changeset<action: nil, changes: %{title: "some title"},
errors: [], data: #Rumbl.Video<>, valid?: true>
But changing a foreign key seems to have no effect:
iex> changeset = Video.changeset(video, %{category_id: 3})
#Ecto.Changeset<action: nil, changes: %{},
errors: [], data: #Rumbl.Video<>, valid?: true>
What should I do to for the changes on the foreign key to be taken into accoung ?
Here is the model
defmodule Rumbl.Video do
use Rumbl.Web, :model
schema "videos" do
field :url, :string
field :title, :string
field :description, :string
belongs_to :user, Rumbl.User, foreign_key: :user_id
belongs_to :category, Rumbl.Category, foreign_key: :category_id
timestamps()
end
#required_fields ~w(url title description)
#optional_fields ~w(category_id)
#doc """
Builds a changeset based on the `struct` and `params`.
"""
def changeset(struct, params \\ %{}) do
struct
|> cast(params, #required_fields, #optional_fields)
|> validate_required([:url, :title, :description])
|> assoc_constraint(:category)
end
end

In Ecto 2.2, the fourth argument to cast is opts, not optional fields. It used to be optional fields earlier which was deprecated in v2.1 with a recommendation to use validate_required instead. This was apparently removed in v2.2.0 although I can't find it in the changelog. You should change your code to this for Ecto 2.2:
struct
|> cast(params, #required_fields ++ #optional_fields)
|> validate_required([:url, :title, :description])
or do this:
#required_fields ~w(url title description)a
#optional_fields ~w(category_id)a
and
|> cast(params, #required_fields ++ #optional_fields)
|> validate_required(#required_fields)

Related

Ecto: changeset drops update to parent model

I’m trying the save an update to a record (user) which has a parent (state). The user record displays fine (in show, edit) including the state. The user record updates fine for it’s own attributes, but any change to the state selected by the user is not persisted. When changing the user’s state from 1 to 2, the user params returned to the controller from the view look fine...from log ->
%{"email" => “e#e.com", "first_name" => “Abe",
"last_name" => “Sam", "password" => “foobar", "state_id" => "2"}
But when this is passed to the user changeset, it comes back with the change to the state_id dropped...from log ->
#Ecto.Changeset<action: nil,
changes: %{password: "foobar",
password_hash: “xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx."},
errors: [], data: #SomeApp.User<>, valid?: true>
The update in the controller is:
def update(conn, %{"id" => id, "user" => user_params}) do
user = Repo.get!(User, id)
changeset = User.changeset(user, user_params)
case Repo.update(changeset) do
   …
The relevant changeset code is:
def changeset(model, params \\ %{}) do
model
|> cast(params, [:email, :first_name, :last_name, :password])
|> assoc_constraint(:state)
|> validate_required([:email, :first_name, :last_name, :password])
|> unique_constraint(:email)
|> validate_length(:password, min: 4, max: 100)
|> put_pass_hash()
end
The schema for the user is:
schema "users" do
field :email, :string
field :password, :string, virtual: true
field :password_hash, :string
field :first_name, :string
field :last_name, :string
belongs_to :state, SomeApp.State
timestamps()
end
What’s am i doing wrong here? Am i missing something in my changeset that casts the user_id from the params into the changeset? I expect I'm doing something simple and stupid - i've genuinely spent hours trying to read up pick this apart and i'm stuck!
Since you want to use the state_id from params, you'll need to add :state_id to the allowed list in the call to cast. Without :state_id in the allowed list, the value will be ignored, which is what is happening right now.
|> cast(params, [:email, :first_name, :last_name, :password])
should be
|> cast(params, [:email, :first_name, :last_name, :password, :state_id])

struct__/1 is undefined, cannot expand struct error Phoenix 1.3

I'm trying to create a contact form in a phoenix 1.3 app. I used mix phx.gen.html to create the relevant files. However, I'm getting a compilation error when trying to start the server:
== Compilation error on file lib/iotc/web/controllers/email_controller.ex ==
** (CompileError) lib/iotc/web/controllers/email_controller.ex:7: Email.__struct__/1 is undefined, cannot expand struct Email
(stdlib) lists.erl:1354: :lists.mapfoldl/3
lib/iotc/web/controllers/email_controller.ex:6: (module)
(stdlib) erl_eval.erl:670: :erl_eval.do_apply/6
Looking at some other posts with a similar issue, it could be related to the alias, but I have alias Itoc.Contact in the controller, and I don't think alias Iotc.Contact.Email would be right here.
email_controller.ex
defmodule Iotc.Web.EmailController do
use Iotc.Web, :controller
alias Iotc.Contact
def index(conn, _params) do
changeset = Email.changeset(%Email{})
emails = Contact.list_emails()
render(conn, "index.html", emails: emails, changeset: changeset)
end
...
email.ex
defmodule Iotc.Contact.Email do
use Ecto.Schema
import Ecto.Changeset
alias Iotc.Contact.Email
schema "contact_emails" do
field :email, :string
field :event, :string
field :message, :string
field :name, :string
timestamps()
end
#doc false
def changeset(%Email{} = email, attrs) do
email
|> cast(attrs, [:name, :email, :message, :event])
|> validate_required([:name, :email, :message, :event])
end
end
With respect to this
OK makes sense. I've updated it to the controller to : changeset =
Contact.Email.changeset(%Contact.Email{}) But I now get: warning:
function Iotc.Contact.Email.changeset/1 is undefined or private. Did
you mean one of: * changeset/2
You only have one function changeset/2 defined in the Email module.
But you're doing Contact.Email.changeset(%Contact.Email{}) passing only one argument. Do ``Contact.Email.changeset(%Contact.Email{})` and it should work.
The /2 part of the signature tells you the arity of the function, namely how many arguments takes.

Zito.Register. __struct__/1 is undefined cannot expand struct

i get the following error whenever i run the mix phoenix.server command. Below is my user.ex model, register_controller.ex and my repo files. Seems to have a problem with my model but i cant really figure what it is.
user.ex
defmodule Zito.User do
use Zito.Web, :model
schema "users" do
field :email, :string
field :crypted_password, :string
timestamps()
end
#doc """
Builds a changeset based on the `struct` and `params`.
"""
def changeset(struct, params \\ %{}) do
struct
|> cast(params, [:email, :crypted_password])
|> validate_required([:email, :crypted_password])
|> validate_format(:email, ~r/#/)
|> validate_length(:crypted_password, min: 8)
|> validate_length(:crypted_password, max: 16)
end
end
(code)
register_controller.ex
defmodule Zito.RegisterController do
use Zito.Web, :controller
alias Zito.Register
def create(conn, %{"register" => register_params}) do
changeset = Register.changeset(%Register{}, register_params)
case Zito.Register.create(changeset, Zito.Repo) do
{:ok, changeset} ->
conn
|> put_flash(:info, "Your account was created")
|> redirect(to: "/")
{:error, changeset} ->
conn
|> put_flash(:info, "Unable to create account")
|> render("register.html", changeset: changeset)
end
end
end
and my repo file
defmodule Zito.Repo.Migrations.CreateUser do
use Ecto.Migration
def change do
create table(:users) do
add :email, :string
add :crypted_password, :string
timestamps()
end
create unique_index(:users, [:email])
end
end

An elegant way to validate and process a complex input using changesets

I'm trying to create a small Phoenix application, and having troubles finding the best way to process user input that came to the model from a controller.
I have 2 models and 1 controller:
defmodule MyApp.Post do
use MyApp.Web, :model
schema "posts" do
field :title, :string
field :text, :string
field :comments_count, :integer
has_many :comments, MyApp.Comment
timestamps()
end
end
defmodule MyApp.Comment do
use MyApp.Web, :model
schema "comments" do
field :text, :string
field :parent_path, :string # I want to store comments in a tree, using "Materialized Path" method
belongs_to :post, MyApp.Post
timestamps()
end
defmodule Ops do
# I keep all operations that are related to comments in this module
alias MyApp.{Repo, Comment}
def create_by_user(params) do
# params came straight from the controller. Expected fields are:
# 1. text - text of the comment, required
# 2. post_id - id of the post, required
# 3. parent_id - id of the parent comment, optional
# This function must:
# 1. Validate presence of the text (this is simple)
# 2. Check that post with given "post_id" exists
# 3. If "parent_id" is given:
# 3.1. Check that parent comment exists and belongs to the same post
# 3.2. Based on fields of parent comment, calculate the "parent_path" value of the new comment
# 4. If input is valid, insert a new comment to database and update post's "comments_count" field
end
end
end
defmodule MyApp.CommentController do
use MyApp.Web, :controller
alias MyApp.{Post, Comment}
def create(conn, params) do
case Comment.Ops.create_by_user(params) do
{:ok, comment} -> conn |> put_status(200) |> json("not implemented yet")
{:error, changeset} -> conn |> put_status(422) |> json("not implemented yet")
# Also, in case of error, it would be nice to look into changeset.errors and if post wasn't found, return 404
end
end
end
What is the most elegant implementation of Comment.Ops.create_by_user function?

Inserting an model into a many-to-many relationship with an existing model in ecto 2

I'm trying out the Ecto 2 rc.
My models are:
schema "containers" do
field :name, :string
many_to_many :items, Test.Item, join_through: Test.ContainerItem, on_delete: :delete_all
timestamps
end
schema "items" do
field :content, :string
many_to_many :containers, Test.Container, join_through: Test.ContainerItem, on_delete: :delete_all
timestamps
end
schema "containers_items" do
belongs_to :container, Test.Container
belongs_to :item, Test.Item
timestamps
end
And my controller code is:
def add_item(conn, %{"item" => item_params, "container_id" => container_id}) do
item = Item.changeset(%Item{}, item_params)
IO.inspect(item) #TODO remove
container = Container |> Repo.get(container_id) |> Repo.preload([:items])
IO.inspect(container) #TODO remove
changeset = container
|> Ecto.Changeset.change
|> Ecto.Changeset.put_assoc(:items, [item])
IO.inspect(changeset) #TODO remove
if changeset.valid? do
Repo.update(changeset)
conn
|> put_flash(:info, "Item added.")
|> redirect(to: container_path(conn, :show, container))
else
render(conn, "show.html", container: container, changeset: changeset)
end
end
Now this works fine if I'm adding a single item to a container. However if an item exists on a container, then trying to add another item gives me:
(RuntimeError) you are attempting to change relation :items of
Test.Container, but there is missing data.
I can't help but feel I'm going about this the wrong way, some advice would be appreciated.
Ok, so I just figured this out.
My problem was is not turning the items into Changesets so that ecto can track the changes that it needs to make.
The only edits I needed to make are to the controller.
It should look like this instead:
def add_item(conn, %{"item" => item_params, "container_id" => container_id}) do
item = Item.changeset(%Item{}, item_params)
IO.inspect(item) #TODO remove
container = Container |> Repo.get(container_id) |> Repo.preload([:items])
IO.inspect(container) #TODO remove
item_changesets = Enum.map([item | container.items], &Ecto.Changeset.change/1)
changeset = container
|> Ecto.Changeset.change
|> Ecto.Changeset.put_assoc(:items, item_changesets)
IO.inspect(changeset) #TODO remove
if changeset.valid? do
Repo.update(changeset)
conn
|> put_flash(:info, "Item added.")
|> redirect(to: container_path(conn, :show, container))
else
render(conn, "show.html", container: container, changeset: changeset)
end
end

Resources