2

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
7
  • Does the change in the changeset include the value for state as an atom or as a string? Commented Jun 30, 2020 at 12:20
  • If it's passed to the changeset, as an atom. Commented Jun 30, 2020 at 12:22
  • Could you post your changeset function as well as the code from where you're updating it? Commented Jun 30, 2020 at 12:40
  • And how about when you update it? I was trying to recreate the issue but it will add the error to the changeset if I try to create it as "draft". In the question you say that you can update the expires_at even when it's a draft, so, can you please also include the code you're using to update it? And does it successfully insert if you just do create_document(%{state: "draft"}), because I tried it and it did return an errored changeset Commented Jun 30, 2020 at 13:38
  • What I mean was then the state is already draft and the expired_at is set, if you try to update the expires_at to nil it will work, when it shouldn't, as being a draft without a expires_at doesn't make sense in this context. Does that makes sense? Commented Jun 30, 2020 at 14:46

1 Answer 1

1

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_changewill 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

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.