I'm having a bit of trouble trying to implement a custom changeset validation. My schema is:
defenum(VersionStateEnum, ["draft", "active"])
schema "versions" do
field :expires_at, :utc_datetime
field :state, VersionStateEnum
end
The validation I'm trying to implement is: The expires_at can only be set if the state is draft (this should also be valid for updates, I should not be able to remove the expires_at if the state is still draft) I tried the following:
defp validate_expires_at(changeset) do
expires_at = get_change(changeset, :expires_at)
cond do
get_change(changeset, :state) == :draft ->
case expires_at do
nil -> add_error(changeset, :expires_at, "can't be blank when state is draft")
_ -> changeset
end
get_change(changeset, :state) == :active ->
case expires_at do
nil -> changeset
_ -> add_error(changeset, :expires_at, "cannot be set when state is not draft")
end
true ->
changeset
end
end
end
But it doesn't really work as I can update the expires_at to nil even if the state is draft. Any help is appreciated.
Edit 1: My changeset:
@required_fields [
:state
]
@optional_fields [:expires_at]
def changeset(model, params \\ nil) do
model
|> cast(params, @required_fields ++ @optional_fields)
|> validate_required(@required_fields)
|> validate_expires_at()
end
Where it's being called:
def create_document(attrs \\ %{}) do
%Document{}
|> Document.changeset(attrs)
|> Repo.insert()
end
If I understand your problem correctly, I think that to solve it you should probably consider the struct given to the changeset as well.
Because, the way your code is, you're only checking for the state
from the changes in the changeset, but if you try to update the expires_at
alone, the changes
in the changeset will not include the state
that might already be set to "draft"
, and therefore, the cond
block in your validate_expires_at
function will always match true
, because the value will be nil
.
One workaround could be to update the function like:
defp validate_expires_at(changeset) do
state = get_field(changeset, :state)
expires_at = get_change(changeset, :expires_at)
case state do
:draft ->
case expires_at do
nil -> add_error(changeset, :expires_at, "can't be blank when state is draft")
_ -> changeset
end
:active ->
case expires_at do
nil -> changeset
_ -> add_error(changeset, :expires_at, "cannot be set when state is not draft")
end
_ -> changeset
end
end
end
Using get_field
instead of get_change
will try to get the field from the changes, but if it wasn't changed, it can be taken from the existing struct and the rest of your function should work normally
Not sure how the atom/String handling works when inserting and retrieving from the DB. You might need to check if
state
could be a String when taken from the changeset's data