phoenix upload file using nested form using arc

513 views Asked by At

I have two models, Product and Images and I want to upload multiple images with product nested form using arc package,

Here is my Product model

defmodule FileUpload.Product do
  use FileUpload.Web, :model

  schema "products" do
    field :name, :string
    field :category, :string
    has_many :images, FileUpload.Image

    timestamps()
  end

  @doc """
  Builds a changeset based on the `struct` and `params`.
  """
  def changeset(struct, params \\ %{}) do
    struct
    |> cast(params, [:name, :category])
    |> validate_required([:name, :category])
  end
end

And my Image model

defmodule FileUpload.Image do
  use FileUpload.Web, :model
  use Arc.Ecto.Schema

  schema "images" do
    field :image, FileUpload.ImageUploader.Type
    belongs_to :product, FileUpload.Product

    timestamps()
  end

  @doc """
  Builds a changeset based on the `struct` and `params`.
  """
  def changeset(struct, params \\ %{}) do
    struct
    |> cast(params, [:image])
    |> cast_attachments(params, [:image])
  end
end

My arc uploader for image model

defmodule FileUpload.ImageUploader do
  use Arc.Definition
  use Arc.Ecto.Definition

  # Include ecto support (requires package arc_ecto installed):
  # use Arc.Ecto.Definition

  @versions [:original]

  # To add a thumbnail version:
  @versions [:original, :thumb]

  # Whitelist file extensions:
  def validate({file, _}) do
    ~w(.jpg .jpeg .gif .png) |> Enum.member?(Path.extname(file.file_name))
  end

  # Define a thumbnail transformation:
  def transform(:thumb, _) do
    {:convert, "-strip -thumbnail 250x250^ -gravity center -extent 250x250 -format png", :png}
  end
  # Override the storage directory:
  def storage_dir(version, {file, scope}) do
    "uploads/product/images/#{scope.id}"
  end
end

My ProductController code for new and create action

defmodule FileUpload.ProductController do
  use FileUpload.Web, :controller

  alias FileUpload.Product

  def index(conn, _params) do
    products = Repo.all(Product)
    render(conn, "index.html", products: products)
  end

  def new(conn, _params) do
    changeset = Product.changeset(%Product{images: [%FileUpload.Image{}]})

    render(conn, "new.html", changeset: changeset)
  end

  def create(conn, %{"product" => product_params}) do
    changeset = Product.changeset(%Product{}, product_params)

    case Repo.insert(changeset) do
      {:ok, _product} ->
        conn
        |> put_flash(:info, "Product created successfully.")
        |> redirect(to: product_path(conn, :index))
      {:error, changeset} ->
        render(conn, "new.html", changeset: changeset)
    end
  end
end

My new product form

<%= form_for @changeset, @action, [multipart: true], fn f -> %>
  <%= if @changeset.action do %>
    <div class="alert alert-danger">
      <p>Oops, something went wrong! Please check the errors below.</p>
    </div>
  <% end %>

  <div class="form-group">
    <%= label f, :name, class: "control-label" %>
    <%= text_input f, :name, class: "form-control" %>
    <%= error_tag f, :name %>
  </div>

  <div class="form-group">
    <%= label f, :category, class: "control-label" %>
    <%= text_input f, :category, class: "form-control" %>
    <%= error_tag f, :category %>
  </div>

  <%= inputs_for f, :images, fn imf -> %>
    <div class="form-group">
      <%= label imf, :image, class: "control-label" %>
      <%= file_input imf, :image, class: "form-control" %>
      <%= error_tag imf, :image %>
    </div>
  <% end %>

  <div class="form-group">
    <%= submit "Submit", class: "btn btn-primary" %>
  </div>
<% end %>

And the mix.ex application and deps block

# Type `mix help compile.app` for more information.
  def application do
    [mod: {FileUpload, []},
     applications: [:phoenix, :phoenix_pubsub, :phoenix_html, :cowboy, :logger, :gettext,
                    :phoenix_ecto, :postgrex, :arc_ecto, :ex_aws, :hackney, :poison]]
  end

  # Specifies your project dependencies.
  #
  # Type `mix help deps` for examples and options.
  defp deps do
    [{:phoenix, "~> 1.2.1"},
     {:phoenix_pubsub, "~> 1.0"},
     {:phoenix_ecto, "~> 3.0"},
     {:postgrex, ">= 0.0.0"},
     {:phoenix_html, "~> 2.6"},
     {:phoenix_live_reload, "~> 1.0", only: :dev},
     {:gettext, "~> 0.11"},
     {:cowboy, "~> 1.0"},
     {:arc, "~> 0.6.0-rc3"},
     {:arc_ecto, "~> 0.5.0-rc1"},
     {:ex_aws, "~> 1.0.0-rc3"},
     {:hackney, "~> 1.5"},
     {:poison, "~> 2.0"},
     {:sweet_xml, "~> 0.5"}
   ]

  end

I can clearly see the images are passed to params

  Parameters: %{"_csrf_token" => "BFwuXTIsfSAKZSRvQRUWNDM3HwsEEAAAhof1YzKxl3WVypRsdFhyrA==", "_utf8" => "✓", "product" => %{"category" => "Shrestha", "images" => %{"0" => %{"image" => %Plug.Upload{content_type: "image/jpeg", filename: "levis_jeans_25.jpg", path: "/tmp/plug-1481/multipart-466112-497804-1"}}}, "name" => "Ramita"}}

But only product is being saved, not the images. Is there something I am missing in my product controller create action. I have watched a nested form video in youtube and there it does nothing in create action.

Please tell me what and where I am missing something.

Thanks

1

There are 1 answers

0
Dogbert On BEST ANSWER

You'll need to add a call to cast_assoc after cast so that Image.changeset/2 is called for each image in Product:

defmodule FileUpload.Product do
  ...
  def changeset(struct, params \\ %{}) do
    struct
    |> cast(params, [:name, :category])
    |> cast_assoc(:images)
    |> validate_required([:name, :category])
  end
end