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

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

Related

How do I create a single controller for two models?

I have two controllers for my MVC structure project. One is going to show a list of expenses and other lists of open sources but I have two different routes for that. How do I create a controller for just one view which will show both lists of expenses and open sources from just one controller?
What's the best way to solve this problem?
defmodule DashboardtaskWeb.OpensourceController do
use DashboardtaskWeb, :controller
alias Dashboardtask.Task
alias Dashboardtask.Task.Opensource
def index(conn, _params) do
opensources = Task.list_opensources()
render(conn, "index.html", opensources: opensources)
end
def new(conn, _params) do
changeset = Task.change_opensource(%Opensource{})
render(conn, "new.html", changeset: changeset)
end
def create(conn, %{"opensource" => opensource_params}) do
case Task.create_opensource(opensource_params) do
{:ok, opensource} ->
conn
|> put_flash(:info, "Opensource created successfully.")
|> redirect(to: Routes.opensource_path(conn, :show, opensource))
{:error, %Ecto.Changeset{} = changeset} ->
render(conn, "new.html", changeset: changeset)
end
end
def show(conn, %{"id" => id}) do
opensource = Task.get_opensource!(id)
render(conn, "show.html", opensource: opensource)
end
def edit(conn, %{"id" => id}) do
opensource = Task.get_opensource!(id)
changeset = Task.change_opensource(opensource)
render(conn, "edit.html", opensource: opensource, changeset: changeset)
end
def update(conn, %{"id" => id, "opensource" => opensource_params}) do
opensource = Task.get_opensource!(id)
case Task.update_opensource(opensource, opensource_params) do
{:ok, opensource} ->
conn
|> put_flash(:info, "Opensource updated successfully.")
|> redirect(to: Routes.opensource_path(conn, :show, opensource))
{:error, %Ecto.Changeset{} = changeset} ->
render(conn, "edit.html", opensource: opensource, changeset: changeset)
end
end
def delete(conn, %{"id" => id}) do
opensource = Task.get_opensource!(id)
{:ok, _opensource} = Task.delete_opensource(opensource)
conn
|> put_flash(:info, "Opensource deleted successfully.")
|> redirect(to: Routes.opensource_path(conn, :index))
end
end
For expenses controller, it's the same. I was thinking to use polymorphic association for this. Do you think it's the best idea?

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

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)

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])

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

Put constraint on PhoenixFramework changeset

I have three models in Phoenix Framework, User, Post, Vote. Users can down vote the post only when they have more than 1 point. The user point is calculated by the up votes their posts got from other users.
This is what I defined in my Vote model:
schema "votes" do
field :type, :integer # -1 0 1
belongs_to :user, News.User
belongs_to :post, News.Post
timestamps()
end
Because the user points is not defined in Vote schema, so I can't use validate_change or add_error directly in model, unless I read other models' data to decide whether to add error to changeset, obviously it will be doing too many things in Vote model.
Where should I put the constraint? Controller or model?
Maybe I should place constraint on the database, make sure the user point never get below zero? I found something like trigger. But how will PostgreSQL trigger return their result to the changeset?
Updated (this one works, but I'm not sure if it's the best way)
I tried it in my controller:
def create(conn, %{"vote" => vote_params}, user) do
changeset = user
|> build_assoc(:votes)
|> Vote.changeset(vote_params)
changeset = if user.point < 1 do
Ecto.Changeset.add_error(changeset, :user_id, "You points is not enough.")
end
case Repo.insert(changeset) do
{:ok, vote} ->
conn
|> put_status(:created)
|> put_resp_header("location", vote_path(conn, :show, vote))
|> render("show.json", vote: vote)
{:error, changeset} ->
conn
|> put_status(:unprocessable_entity)
|> render(WechatNews.ChangesetView, "error.json", changeset: changeset)
end
end
It's easy, but I have to repeat it in the update action too.
You could do something along the lines of
def changeset(struct, params) do
struct
|> cast([:points], params)
|> validate_required(:points)
end
def point_changeset(struct, params) do
struct
|> changeset(params)
|> check_points
end
defp check_points(changeset) do
if get_field(changeset, :points) < 1 do
add_error(changeset, :points, "too low")
else
changeset
end
end
This would give you a function point_changeset/2, which you can use only when you need to check that a user can do a certain action based on their points. It still calls the main changeset/2 function that has your validations that you want to always run.

Resources