How to store a datetime, but show a boolean?

I have a field called paused_at on one of my Models.

I want users to toggle a boolean checkbox called paused in the frontend, but then save the time at which they paused it.

So far I have the following:

  1. in the model:
  schema "tablename" do
     # ... code before here omitted for brevity
     field :paused_at, Ecto.DateTime
     field :paused, :boolean, virtual: true
  end

# ... code omitted for brevity

  def changeset(model, params \\ :empty) do
    model
    |> cast(params, @required_fields, @optional_fields)
    |> set_paused_at
  end

  
  def set_paused_at(changeset = %Ecto.Changeset{changes: %{paused: nil}}) do
    changeset
    |> put_change(:paused_at, Ecto.DateTime.from_erl(:erlang.universaltime))
  end
  def set_paused_at(changeset = %Ecto.Changeset{}), do: changeset

Storing the correct value now works. But where should I add code that adds the proper boolean value to paused whenever the model is queried?

6 Likes

Can you show your template/html also please? I if understood correctly you have field paused :: boolean and paused_at :: Ecto.Datetime and when the user clicks the checkbox paused == true and paused_at == current_time, right?

This will depend how you’re doing things exactly but from your code my initial suggestion would be updating the paused value in side the set_paused_at. If you’re already inserting the current time value then it means it was also stopped and therefore it makes sense to also update it in the same place.

Otherwise in your template you may have an hidden_input that send the correct boolean value to be updated in the controller. But again, this largely depends on how your code is behaving and organized.

2 Likes

Here is the HTML form:

- form_for @changeset, @action, [class: "ui form"], fn f ->
  - if @changeset.action do
    .ui.message.danger
      %p
        Oops, something went wrong! Please check the errors below.

  .field
    = label f, :cost
    = number_input f, :cost, step: "any"
    = error_tag f, :cost

  .fields
    .field
      = label f, :end_date
      = datetime_select f, :end_date
      = error_tag f, :end_date
  
  .field
    =label f, :paused
    = checkbox f, :paused
    = error_tag f, :paused
    
  .field
    = submit "Submit", class: "ui primary button"

You indeed understand it correctly.

The main problem I now have, is how to show again a boolean in the User Interface.
Right now I have a function called Subscription.paused?/1 that checks the paused_at, but what I’d rather want to do, is to set the boolean correctly when retrieving the struct from the database.

Is that possible?

1 Like

@Qqwy I’m having a hard time understanding your problem :confounded: You mean you want to render the paused value in your eex/html file? You usually do that with something like this: <%= subscription.paused %>. If you want to use other values instead of true and false you can do <%= if subscription.paused do "Paused" else "Unpaused" end.

1 Like

I’m sorry for explaining it poorly :sweat_smile:.

I want to set the virtual field before returning the %Subscription{} struct to wherever it is used (in the html view, in the json api). This is logic that belongs in the model, but as functions like Repo. all(Subscription) are called in the controller, I am not sure how to do that.

In an OOP-language I would simply add a method to my model class, because data and code is intertwined there. In functional land, however, this is not possible.

3 Likes

Why can’t you alias the App.Repo module and use it in the model as well?

1 Like

Instead of using the Repo in the model (which indeed is easy), I want to use the virtual field inside the controllers/views.

Let me describe my thought process from a slightly higher level.

As most example code only talks to the Repo in the controller, I see the following options:

  1. Use the Repo in the model, only use models made methods(that internally define the correct value for the virtual field) in the controllers. (ˋSubscription.allˋ instead of ˋRepo.all(Subsciption)ˋ. This would take a lot of work, and I am not sure if this extra abstraction layer is good, or moves the DB related stuff to a place it doesn’t belong.
  2. Do not use virtual fields this way. Instead of reading values from a struct, have methods in your model that return values on the Struct (ˋSubscription.name(subscription)ˋ instead of ˋsubscription.nameˋ ). This means that it would be easier to in the future maybe wrap some of the other fields as well, but it would be a lot more verbose.
  3. Override the functions in the Repo with new variants that in the case of a single (or a few) models do extra behaviour on top of what Ecto.Repo provides. Seems like a hackish idea, and also a lot of work. The advantage would of course be that you are able to use exactly the same code to look up a model as before. But this might get the easy vs simplicity thing wrong. (It is very much the Activerecord style solution)
3 Likes

I found this thread eight months after the last comment on it.

I have a similar question and fully understand your problem (or frustration).

Here is my tentative solution:

  def changeset(struct, params \\ %{}) do
    struct
    |> populate
    |> cast(params, [:name, :paused])
    |> set_paused_at
  end

  defp populate(struct) do
    Map.merge(struct, %{paused: struct.paused_at != nil})
  end

  defp set_paused_at(changeset = %Ecto.Changeset{changes: %{paused: true}}) do
    changeset
    |> put_change(:paused_at, Ecto.DateTime.from_erl(:erlang.universaltime))
  end
  defp set_paused_at(changeset = %Ecto.Changeset{changes: %{paused: false}}) do
    changeset
    |> put_change(:paused_at, nil)
  end
  defp set_paused_at(changeset), do: changeset

I define the function populate/1 to set a boolean value to the virtual field paused. You can think it as a kind of after_initialize callback of Rails.

I am glad if someone gives me a review on this code.

1 Like