ex_machina icon indicating copy to clipboard operation
ex_machina copied to clipboard

Question: Changesets

Open BenMorganIO opened this issue 7 years ago • 6 comments

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?

BenMorganIO avatar Apr 02 '17 20:04 BenMorganIO

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 - casting 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).

pdawczak avatar Apr 02 '17 20:04 pdawczak

@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.

paulcsmith avatar Apr 03 '17 14:04 paulcsmith

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

rjurado01 avatar Jul 22 '19 17:07 rjurado01

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.

blcksheep80 avatar Oct 17 '19 09:10 blcksheep80

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.

nickjj avatar Jan 19 '20 00:01 nickjj

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?

dennym avatar Feb 26 '20 16:02 dennym

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.

cospin avatar Oct 02 '22 02:10 cospin