Ecto: changeset drops update to parent model - phoenix-framework

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

Related

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)

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

Insert Ecto model with already existing model as an association

I have 2 models, entries:
schema "entries" do
belongs_to :exception, Proj.Exception
field :application, :string
end
And exceptions:
schema "exceptions" do
field :name, :string
end
The migration script:
def change do
create table(:exceptions) do
add :name, :string, null: false
end
create table(:entries) do
add :exception_id, references(:exceptions), null: false
add :application, :string, null: false
end
end
My goal is to store exceptions that happen in another system. I want the project to be able to store each exception in the second table exception if they are not already there and then store the application name and the exception id in the first table entries. There will be 1000s of records in entries and a handful in exceptions.
Assuming entry_params uses this JSON format:
{
exception: "NPE",
application: "SomeApp"
}
the method that should create the entries:
def create(conn, %{"entry" => entry_params}) do
exception = Repo.get_by(Exception, name: entry_params["exception"]) ||
Repo.insert!(%Exception{name: entry_params["exception"]})
changeset =
Entry.changeset(%Entry{}, entry_params)
|> Ecto.Changeset.put_assoc(:exception, exception)
Repo.insert!(changeset)
end
This will print out:
** (ArgumentError) unknown assoc `exception` in `put_assoc`
If I change the entries model to use has_one instead of belongs_to (and I think belongs_to "feels" bad here. An entry does not belong to an exception, it just has an exception) it throws the following:
** (Postgrex.Error) ERROR (not_null_violation): null value in column "exception_id" violates not-null constraint
table: entries
column: exception_id
What I want basically to first create an Exception (if it does not exist) and than create a new Entry of a system error and put the previously begotten Exception in the entry as an association.
What is wrong here?
Typo. belongs_to :exception, Proj.Exception should be belongs_to :exceptions, Proj.Exception
Association. Based on the data model in the question, I think the put_assoc is the wrong way around because in the data schema in the question, an exception has_many entries and an entry belongs_to exceptions. Ecto.Changeset.put_assoc(entries_changeset, :exception, exception) should be Ecto.Changeset.put_assoc(exception_changeset, :entries, entries)
Attempted solution:
entries schema:
schema "entries" do
field :application, :string
belongs_to :exceptions, Proj.Exception, on_replace: :nilify
end
exceptions schema:
schema "exceptions" do
field :name, :string
has_many :entry, Proj.Entry, on_delete: :delete_all, on_replace: :delete
end
migration script:
def change do
create table(:exceptions) do
add :name, :string, null: false
end
create table(:entries) do
add :application, :string, null: false
add :exception_id, references(:exceptions)
end
end
Assuming entry_params uses this JSON format:
{
exception: "NPE",
application: "SomeApp"
}
create or update the exceptions and the associated entries:
def create(conn, %{"entry" => entry_params}) do
new_entry = Entry.changeset(%Entry{}, entry_params)
changeset =
case Repo.get_by(Exception, name: entry_params["exception"]) do
:nil ->
exception = %Exception{name: entry_params["exception"]} |> Repo.insert!
Ecto.Changeset.build_assoc(exception, :entries, [new_entry])
struct ->
changeset = Ecto.Changeset.change(struct)
data = Ecto.Changeset.preload(changeset, :entries) |> Map.get(:model) # Ecto 1.x
# data = Ecto.Changeset.preload(changeset, :entries) |> Map.get(:data) # Ecto 2.0.x
Ecto.Changeset.put_assoc(changeset, :entries, [new_entry | data.entries])
end
Repo.insert!(changeset)
end

Rails -- updating a model in a database

So I ran into a little issue with validations -- I created a validation to ensure that no users in a database share identical email addresses. Then I created a user in the database. Afterward, I said user = User.find(1) which returned the user I had just created. Then I wanted to change its name so I said user.name = "New Name" and then tried to use user.save to save it back into the database. However, this command isn't working anymore (it returns false instead) and I think it has to do with my uniqueness validation test. Can someone help me with this problem?
# == Schema Information
#
# Table name: users
#
# id :integer not null, primary key
# name :string(255)
# email :string(255)
# created_at :datetime
# updated_at :datetime
#
class User < ActiveRecord::Base
attr_accessor :password
attr_accessible :name, :email, #says that the name and email attributes are publicly accessible to outside users.
:password, :password_confirmation #it also says that all attributes other than name and email are NOT publicly accessible.
#this protects against "mass assignment"
email_regex = /^[A-Za-z0-9._+-]+#[A-Za-z0-9._-]+\.[A-Za-z0-9._-]+[A-Za-z]$/ #tests for valid email addresses.
validates :name, :presence => true,
:length => {:maximum => 50}
validates :email, :presence => true,
:format => {:with => email_regex},
:uniqueness => {:case_sensitive => false}
validates :password, :presence => true,
:length => {:maximum => 20, :minimum => 6},
:confirmation => true
before_save :encrypt_password
def has_password?(submitted_password)
#compare encrypted_password with the encrypted version of the submitted password.
encrypted_password == encrypt(submitted_password)
end
def self.authenticate(email, submitted_password)
user = find_by_email(email)
if (user && user.has_password?(submitted_password))
return user
else
return nil
end
end
private
def encrypt_password
if (new_record?) #true of object has not yet been saved to the database
self.salt = make_salt
end
self.encrypted_password = encrypt(password)
end
def encrypt(string)
secure_hash("#{salt}--#{string}")
end
def secure_hash(string)
Digest::SHA2.hexdigest(string) #uses cryptological hash function SHA2 from the Digest library to encrypt the string.
end
def make_salt
secure_hash("#{Time.now.utc}--#{password}")
end
end
try save! and see what the exception tells you

Ruby / Datamapper create or update issue - immutable error

I'm have a user class that can optionally have a billing address. When I post a payment form, assuming the user has indicated they want to save their billing address details, I want to either create a new address record or update the original one.
I have tried many things but the closest I can get to working code is...
class User
include DataMapper::Resource
property :id, Serial
property :provider, String, :length => 100
property :identifier, String, :length => 100
property :username, String, :length => 100
property :remember_billing, Boolean
has 1, :billing_address
end
class BillingAddress
include DataMapper::Resource
property :first, String, :length => 20
property :surname, String, :length => 20
property :address1, String, :length => 50
property :address2, String, :length => 50
property :towncity, String, :length => 40
property :state, String, :length => 2
property :postcode, String, :length => 20
property :country, String, :length => 2
property :deleted_at, ParanoidDateTime
belongs_to :user, :key => true
end
post "/pay" do
#post = params[:post]
#addr = params[:addr]
if #addr == nil
#addr = Hash.new
end
user = User.first(:identifier => session["vya.user"])
user.remember_billing = !!#post["remember"]
if user.remember_billing
user.billing_address = BillingAddress.first_or_create({ :user => user }, #addr)
end
user.save
...
which works fine when there is no record. But if there is already a record, it keeps the original values.
I saw a similar post
DataMapper: Create new record or update existing
but if I alter the code to be
user.billing_address = BillingAddress.first_or_create(:user => user).update(#addr)
I get the error
DataMapper::ImmutableError at /pay
Immutable resource cannot be modified
Any help much appreciated
You're chaining lots of things together, there. How about:
billing = BillingAddress.first_or_new(:user => user, #addr) #don't update, send the hash as second parameter
billing.saved? ? billing.update(#addr) : billing.save
raise "Billing is not saved for some reason: #{billing.errors.inspect}" unless billing && billing.saved?
user.billing_address = billing
user.save

Resources