ex_machina
ex_machina copied to clipboard
Question: Changesets
Hey there,
I'm currently using ExMachina and trying to integrate it into my project. I was hoping it would clean up a number of changesets that I'm creating throughout the project, yet it appears all ExMachina does is setup structs with prefilled data.
Is there a way to have the ExMachina build changesets instead of structs?
I don't think this is supported currently (you are right - ExMachina supports structs and maps only).
For now, the shortest way I can think of to address your problem would be using Ecto.Changeset.change/2
.
Consider this:
Ecto.Changeset.change(%MyApp.User{}, params_for(:user, name: "Joe Tester"))
# => #Ecto.Changeset<action: nil, changes: %{name: "Joe Tester"}, errors: [],
# data: #MyApp.User<>, valid?: true>
This builds the changeset for you, but I'm not quite sure it is exactly what you need - if you want to got through your proper function building changeset (by that I mean - cast
ing params, performing validation or even put_change
) - this might be very specific to exact use case and as such, quite difficult to build some framework around it.
It might be easier and faster just to execute proper function with params (eg. MyApp.User.changeset
).
@BenMorganIO Here is a comment (along with other helpful comments above it) that could help: https://github.com/thoughtbot/ex_machina/issues/82#issuecomment-255592753
I would consider adding more support for changesets, but I think adding a function to handle it might make sense. Can you include an example(s) changeset, test and factory so that I can get a better idea of how you're using this? Would extracting a function as suggested work for you?
Maybe we just need to add something to the README to guide people toward using a function for doing changeset specific changes.
What about this?
User model:
def changeset(user, attrs) do
user
|> cast(attrs, [:email, :is_active, :password])
|> validate_required([:email, :is_active, :password])
|> validate_format(:email, ~r/@/)
|> validate_length(:password, min: 8)
|> unique_constraint(:email)
|> put_password_hash()
end
def changeset(attrs) do
changeset(%App.User{}, attrs)
end
Factory:
defmodule App.UserFactory do
defmacro __using__(_opts) do
quote do
def user_factory do
attrs = %{
email: sequence(:email, &"user#{&1}@email.com"),
is_active: true,
password: "12345678"
}
struct(%App.User{}, App.User.changeset(attrs).changes)
end
end
end
end
I'm also battling a little with understanding how to get ExMachina working with my changesets :(
I have the following https://github.com/blcksheep80/blcksheepio-api/blob/master/lib/blcksheepio_api/factory.ex which is used (at least for now) in https://github.com/blcksheep80/blcksheepio-api/blob/master/lib/mix/tasks/seed.ex
However, whenever I run the task it doesn't seem to hit the changeset defined in https://github.com/blcksheep80/blcksheepio-api/blob/master/lib/blcksheepio_api/classification/category.ex
Any ideas would be most appreciated. Thanks.
I would also be interested in being able to go through our app level changesets.
The use case (and problem) I ran into is I'm using https://github.com/dmarkow/ecto_ranked to auto-populate a rank
field in the database to handle user defined ordering of the items.
The problem is it's expected that you add |> EctoRanked.set_rank()
to your changeset. If this does not get run then you end up with a bunch of nil rank
fields.
Hey there,
today I also stumbled into this issue. Since I have a User and Credential schema in my Accounts context (from the cms example out of the phoenix framework documentation). I wanted to make this work for my session controller tests.
# factory
def user_factory do
%User{
name: "#{Faker.Name.En.name()}",
username: "#{Faker.Name.En.name()}",
credential: build(:credential)
}
end
def credential_factory do
%Credential{
email: "#{Faker.Internet.email()}",
password_hash: "unreadable"
}
end
def set_password(user, password) do
user.credential
|> Credential.changeset(%{password: password})
|> Ecto.Changeset.apply_changes()
user
end
# in the test
current_user = build(:user) |> set_password("123123") |> insert
This is hacked together also from #82 . The changeset/2
of credential gets called correctly and applies the changes correctly. Unfortunately this is not reflected in the user
return struct of set_password/2
. Does someone can give me some insight or guidance here?
I also had to deal with this problem. Putting together what the others have mentioned, I ended up with this:
# user_factory.ex
def user_factory do
%User{
first_name: Faker.Person.En.first_name(),
last_name: Faker.Person.En.last_name()
# ...
}
end
# factory.ex
def with_password(%{} = user, password \\ Core.Generator.password(:valid)) do
apply_changeset(user, %{password: password})
end
def apply_changeset(%type{} = struct) do
struct(type)
|> type.changeset(Map.from_struct(struct))
|> Ecto.Changeset.apply_changes()
|> insert()
end
def apply_changeset(%{} = struct, args) do
Map.merge(struct, args)
|> apply_changeset()
end
I have the function apply_changeset/1
and /2
which should work for any shcema. It receives a struct, which is generated by build()
, and for convenience apply_changeset
does the insert()
too, also with_password/2
is just apply_changeset/2
, I made it for convenience too to avoid more code and maps in my tests, so:
# simpler way for password set: (add your own for any specific changeset transform)
user = build(:user) |> with_password()
user = build(:user) |> with_password("mypassword")
# using with_changeset, here you can put anything, not just password.
user = build(:user) |> apply_changeset()
user = build(:user) |> apply_changeset(%{password: "mypassword"})
user = build(:user, %{password: "mypassword"}) |> apply_changeset()
If you are already putting the password in your user_factory, just call apply_changeset/1
Note: ex_machina insert() will not raise any error if there is something wrong with applying the changeset, it just discards the invalid changes, if you need this to raise, change the insert() to your Repo.insert()
Any suggestion is welcome.