2

I'm missing something basic with my phoenix code

This is in my controller

  def show(conn, %{"id" => id}) do
    user = Repo.get!(User, id)

    query =
        from c in Card,
            where: c.user_id == 1,
            select: {c.id, c.inserted_at, c.estimate_name, c.product}
    estimates = Repo.all(query)

    render(conn, "show.json", %{user: user, estimates: estimates})
    # render(conn, "show.json", user: user)
  end

And in my view

  # def render("show.json", %{user: user}) do
  def render("show.json", %{user: user, estimates: estimates}) do
    %{data: render_one(%{user: user, estimates: estimates}, Api.UserView, "user.json") }
    # %{data: render_one(user, Api.UserView, "user.json")}

    #  ** New code since original post **
    # %{data: render("user.json", %{user: user, estimates: estimates})}
  end

  def render("user.json", %{user: user, estimates: estimates}) do
  # def render("user.json", %{user: user}) do
    %{id: user.id,
      firstname: user.firstname,
      lastname: user.lastname,
      email: user.email,
      customerId: user.customerId,
      estimates: render("estimates.json", estimates)}  # **Line with last error**
  end

  def render("estimates.json", [head | _estimates]) do
    #   Enum.map(estimates, fn estimate -> render(mapper estimate, MosaicApi.UserView, "summaryEstimate.json") } end)
    # render(mapper(head), MosaicApi.UserView, "summaryEstimate.json")
    render("summaryEstimate.json", mapper(head))

  end

  # ** I'm fear I have added unnecessary complexity here **
  def mapper({id, date, name, product}) do
      %{id: id,
        creation_date: date,
        estimate_name: name,
        product: product}
  end

  def render("summaryEstimate.json", estimate) do
    %{id: estimate.id,
      estimate_name: estimate.estimate_name,
      product: estimate.product}
  end

But I get an error, which seems to be because my data has morphed from %{user: ..., estimates: [...]} to %{user: %{estimates:[...], user: %{...}}.

How did that happen, and how can I prevent it?

Could not render "user.json" for MosaicApi.UserView, please define a matching clause for render/2 or define a template at "web/templates/user". No templates were compiled for this module.

Assigns:

%{user: %{estimates: [{1, #Ecto.DateTime<2016-04-26T12:01:34Z>, "cards for annabelle", "Business Cards"}, ...], user: %Api.User{__meta__: #Ecto.Schema.Metadata<:loaded>, customerId: "CUST", email: "foo@foo.be", firstname: "fname 1", id: 1, inserted_at: #Ecto.DateTime<2016-04-26T11:46:21Z>, jobs: #Ecto.Association.NotLoaded<association :jobs is not loaded>, lastname: "lname 1", updated_at: #Ecto.DateTime<2016-04-26T11:46:21Z>}}, view_module: Api.UserView, view_template: "user.json"}
Simon H
  • 20,332
  • 14
  • 71
  • 128

2 Answers2

3

The render_one/4 and render_many/4 functions inflect the assigns name from the view. I explain this in Render many to many relationship JSON in Phoenix Framework

So what happens when you call:

render_one(%{user: user, estimates: estimates}, Api.UserView, "user.json")

Is that render is called with the following arguments:

render("user.json", %{user: %{user: %{...}, estimates: [...]})

You can change the name of assigns using as:

render_one(%{user: user, estimates: estimates}, Api.UserView, "user.json", as: :data)

Which will call render with:

render("user.json", %{data: %{user: %{...}, estimates: [...]})

You can match this with:

def render("user.json", %{data: %{user: user, estimates: estimates}}) do

You can avoid this by calling render directly instead of using render_one:

render("user.json", %{user: user, estimates: estimates})

Another option is to use a data structure that nests the estimates inside of the user struct. Perhaps by adding a virtual attribute to your schema.

EDIT

  def render("show.json", %{user: user, estimates: estimates}) do
    %{data: render("user.json", %{user: user, estimates: estimates})}
  end

  def render("user.json", %{user: user, estimates: estimates}) do
    %{id: user.id,
      ...
      estimates: render_many(estimates, __MODULE__, "estimates.json", estimates. as: estimate)}
  end

  def render("estimates.json", %{estimate: {id, _date, name, product}}) do
    %{id: id,
      estimate_name: name,
      product: product}
  end
Gazler
  • 83,029
  • 18
  • 279
  • 245
  • That made a big difference, but I'm now getting `argument error (stdlib) :maps.from_list([{1, #Ec...`. I've added some more code. I thought my query was delivering a list and that I could pattern match against that, but not sure that that is in fact the case – Simon H Apr 27 '16 at 12:38
  • Looks like you went off track a little, check my edit. – Gazler Apr 27 '16 at 13:05
  • THANKS!! Looks like you had a typo as it eventually needed `estimates: render_many(estimates, __MODULE__, "estimates.json", as: :estimate)}` – Simon H Apr 27 '16 at 13:18
3

How did that happen, and how can I prevent it?

That's because Phoenix.View.render_one wrapped your assigns to be in a key inflected from the view's name.

From its docs:

The following:

render_one user, UserView, "show.html"

is roughly equivalent to:

if user != nil do
  render(UserView, "show.html", user: user)
end

The underlying user is passed to the view and template as :user, which is inflected from the view name.

You should just call render/2 directly:

def render("show.json", %{user: user, estimates: estimates}) do
  %{data: render("user.json", %{user: user, estimates: estimates}}
end
Dogbert
  • 212,659
  • 41
  • 396
  • 397
  • That made a big difference, but I'm now getting `argument error (stdlib) :maps.from_list([{1, #Ec...`. I've added some more code. I thought my query was delivering a list and that I could pattern match against that, but not sure that that is in fact the case – Simon H Apr 27 '16 at 12:38