Kapeusz

Kapeusz

Hello folks! I am trying to upload multiple files at once using Arc. I face such error: protocol Enumerable not implemented for %Plug.Upload when i try to create a product with attached file/s. Any idea what I am doing wrong here?

My schemas:

  schema "products" do
    field :colour, :string
    field :description, :string
    field :name, :string
    field :price, :decimal
    field :product_code, :string
    field :release_year, :integer

    belongs_to :category, Jordaniva.Categories.Category
    belongs_to :subcategory, Jordaniva.Categories.Subcategory
    belongs_to :type, Jordaniva.Inventory.Type

    has_many :items, Jordaniva.Inventory.Item
    has_many :photos, Jordaniva.Gallery.Photo

    timestamps()
  end

  @doc false
  def changeset(struct, attrs \\ %{}) do
    struct
    |> cast(attrs, [:name, :description, :price, :colour, :product_code, :release_year, :category_id, :subcategory_id, :type_id])
    |> cast_assoc(:items)
    |> cast_assoc(:photos)
    |> validate_required([:name, :description, :price, :colour, :product_code, :release_year, :category_id, :subcategory_id, :type_id])
  end
  schema "photos" do
    field :photo, Jordaniva.Photo.Type
    field :uuid, :string

    belongs_to :product, Jordaniva.Inventory.Product

    timestamps()
  end

  @doc false
  def changeset(photo, attrs) do
    photo
    |> Map.update(:uuid, Ecto.UUID.generate, fn val -> val || Ecto.UUID.generate end)
    |> cast_attachments(attrs, [:photo])
    |> validate_required([:photo])
  end

Product controller (new/create):

  def new(conn, _params) do
    changeset = Product.changeset(%Product{items: [%Item{}, %Item{}]})
    categories = Repo.all(Category) |> Enum.map(&{&1.name, &1.id})
    subcategories = Repo.all(Subcategory) |> Enum.map(&{&1.name, &1.id})
    render(conn, "new.html", changeset: changeset, categories: categories, subcategories: subcategories)
  end

  def create(conn, %{"photos" => product_params}) do
    case Inventory.create_product(product_params) do
      {:ok, product} ->
        conn
        |> put_flash(:info, "Product created successfully.")
        |> redirect(to: Routes.product_path(conn, :show, product))
      {:error, %Ecto.Changeset{} = changeset} ->
        categories = Repo.all(Category) |> Enum.map(&{&1.name, &1.id})
        subcategories = Repo.all(Subcategory) |> Enum.map(&{&1.name, &1.id})
        render(conn, "new.html", changeset: changeset, categories: categories, subcategories: subcategories)
    end
  end

Photo controller:

  def new(conn, _params) do
    changeset = Gallery.change_photo(%Photo{})
    render(conn, "new.html", changeset: changeset)
  end

  def create(conn, %{"product_id" => product_id, "photo" => photo_params}) do
    product = Jordaniva.Inventory.get_product!(product_id)
    case Gallery.create_photo(photo_params) do
      {:ok, _photo} ->
        conn
        |> put_flash(:info, "Photo created successfully.")
        |> redirect(to: Routes.photo_path(conn, :index))

      {:error, %Ecto.Changeset{} = changeset} ->
        render(conn, "new.html", changeset: changeset)
    end
  end

Gallery.ex

  def create_photo(%Product{} = product, attrs \\ %{}) do
    %Photo{}
    |> Photo.changeset(attrs)
    |> Ecto.Changeset.put_assoc(:product, product)
    |> Repo.insert()
  end
  

  def create_photos(%Product{} = product, attrs \\ %{}) do
    Enum.each attrs["photos"], fn p ->
      create_photo(product, %{"photo" => p})
    end
  end
end

Uploaders/photo.ex

defmodule Jordaniva.Photo do
  use Arc.Definition
  use Arc.Ecto.Definition

  @extension_whitelist ~w(.jpg .jpeg .gif .png)

  @versions [:primary]
  @acl :public_read

  def default_url(:primary, _movie) do
    "http://placehold.it/350x200"
  end

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

  # Whitelist file extensions:
  def validate({file, _}) do
    file_extension = file.file_name |> Path.extname() |> String.downcase()
    Enum.member?(@extension_whitelist, file_extension)
  end

  # Define a thumbnail transformation:
  def transform(:thumb, _) do
    {:convert, "-strip -thumbnail 150x150^ -gravity center -extent 150x150"}
  end

  # Override the persisted filenames:
  def filename(version, {file, scope}) do
    "#{scope.uuid}_#{version}"
  end

  # Override the storage directory:
  def storage_dir(version, {file, scope}) do
    "uploads/photos/"
  end
end

And full error:

[info] POST /products
[debug] Processing with JordanivaWeb.ProductController.create/2
  Parameters: %{"_csrf_token" => "DQAuSi4VWwZsAgQxSltSVwYCMSkSTnR2_b_ggd30Us4dz-jesRTyb897", "photos" => %{"category_id" => "1", "colour" => "test66", "description" => "test66", "name" => "test66", "photos" => [%Plug.Upload{content_type: "image/png", filename: "Zrzut ekranu 2020-07-20 o 22.46.06.png", path: "/var/folders/1t/q27498654m376jz6fl02fqd80000gn/T//plug-1597/multipart-1597933231-847704270337599-3"}], "price" => "666", "product_code" => "test66", "release_year" => "2004", "subcategory_id" => "1", "type_id" => "1"}}
  Pipelines: [:browser]
[debug] QUERY OK source="types" db=0.3ms idle=1764.5ms
SELECT t0."id", t0."name", t0."inserted_at", t0."updated_at" FROM "types" AS t0 ORDER BY t0."name" []
[info] Sent 500 in 32ms
[error] #PID<0.1214.0> running JordanivaWeb.Endpoint (connection #PID<0.1213.0>, stream id 1) terminated
Server: localhost:4000 (http)
Request: POST /products
** (exit) an exception was raised:
    ** (Protocol.UndefinedError) protocol Enumerable not implemented for %Plug.Upload{content_type: "image/png", filename: "Zrzut ekranu 2020-07-20 o 22.46.06.png", path: "/var/folders/1t/q27498654m376jz6fl02fqd80000gn/T//plug-1597/multipart-1597933231-847704270337599-3"} of type Plug.Upload (a struct). This protocol is implemented for the following type(s): Ecto.Adapters.SQL.Stream, Postgrex.Stream, DBConnection.Stream, DBConnection.PrepareStream, HashSet, Range, Map, Function, List, Stream, Date.Range, HashDict, GenEvent.Stream, MapSet, File.Stream, IO.Stream
        (elixir 1.10.4) lib/enum.ex:1: Enumerable.impl_for!/1
        (elixir 1.10.4) lib/enum.ex:141: Enumerable.reduce/3
        (elixir 1.10.4) lib/enum.ex:3383: Enum.reduce/3
        (arc_ecto 0.11.3) lib/arc_ecto/schema.ex:58: Arc.Ecto.Schema.convert_params_to_binary/1
        (jordaniva 0.1.0) lib/jordaniva/gallery/photo.ex:19: Jordaniva.Gallery.Photo.changeset/2
        (ecto 3.4.5) lib/ecto/changeset.ex:829: anonymous fn/4 in Ecto.Changeset.on_cast_default/2
        (ecto 3.4.5) lib/ecto/changeset/relation.ex:128: Ecto.Changeset.Relation.do_cast/6
        (ecto 3.4.5) lib/ecto/changeset/relation.ex:338: Ecto.Changeset.Relation.map_changes/9
        (ecto 3.4.5) lib/ecto/changeset/relation.ex:112: Ecto.Changeset.Relation.cast/5
        (ecto 3.4.5) lib/ecto/changeset.ex:800: Ecto.Changeset.cast_relation/4
        (jordaniva 0.1.0) lib/jordaniva/inventory/product.ex:29: Jordaniva.Inventory.Product.changeset/2
        (jordaniva 0.1.0) lib/jordaniva/inventory.ex:67: Jordaniva.Inventory.create_product/1
        (jordaniva 0.1.0) lib/jordaniva_web/controllers/product_controller.ex:28: JordanivaWeb.ProductController.create/2
        (jordaniva 0.1.0) lib/jordaniva_web/controllers/product_controller.ex:1: JordanivaWeb.ProductController.action/2
        (jordaniva 0.1.0) lib/jordaniva_web/controllers/product_controller.ex:1: JordanivaWeb.ProductController.phoenix_controller_pipeline/2
        (phoenix 1.5.4) lib/phoenix/router.ex:352: Phoenix.Router.__call__/2
        (jordaniva 0.1.0) lib/jordaniva_web/endpoint.ex:1: JordanivaWeb.Endpoint.plug_builder_call/2
        (jordaniva 0.1.0) lib/plug/debugger.ex:132: JordanivaWeb.Endpoint."call (overridable 3)"/2
        (jordaniva 0.1.0) lib/jordaniva_web/endpoint.ex:1: JordanivaWeb.Endpoint.call/2
        (phoenix 1.5.4) lib/phoenix/endpoint/cowboy2_handler.ex:65: Phoenix.Endpoint.Cowboy2Handler.init/4

Showing Posts 1 to 10

jeroenbourgois

jeroenbourgois

Have you found something in the mean while? If not, I will try to dive in.

al2o3cr

al2o3cr

The error you’re getting points to this code in arc_ecto:

https://github.com/stavro/arc_ecto/blob/1623e67076bec38efd40929fab3ac7c8f258b476/lib/arc_ecto/schema.ex#L57-L69

and suggests that it’s getting a Plug.Upload struct instead of a map %{"photo": %Plug.Upload{}}

Try IO.inspecting things until you find where the parameter shape doesn’t match what you expect.

Kapeusz

Kapeusz OP

Sadly no, I have not found solution.

Kapeusz

Kapeusz OP

Thanks for your reply, I will IO.inspect my code and try to find where the parameters don’t match.

Kapeusz

Kapeusz OP

I’ve made tiny changes to my code, and right now, I do not see any error message, but the problem is I cannot upload any file/s. When I add a new image and click save it shows “Oops, something went wrong! Please check the errors below.” but doesn’t display anything below. In the terminal, it looks like that:

[debug] Processing with JordanivaWeb.ProductController.create/2
  Parameters: %{"_csrf_token" => "Ewo7HQVpOGEiIxsdHkk4TUMsPCsQBR0VpNId63iUIUmRndr72bVramuY", "photos" => %{"category_id" => "1", "colour" => "test1", "description" => "test1", "name" => "test1", "photos" => %{"0" => %{"photos" => [%Plug.Upload{content_type: "text/html", filename: "+ud6zQ2o.html", path: "/var/folders/1t/q27498654m376jz6fl02fqd80000gn/T//plug-1598/multipart-1598452423-991068043046336-5"}, %Plug.Upload{content_type: "image/jpeg", filename: "air_jordan1.jpg", path: "/var/folders/1t/q27498654m376jz6fl02fqd80000gn/T//plug-1598/multipart-1598452423-154324122449949-5"}]}}, "price" => "1111", "product_code" => "test1", "release_year" => "1111", "subcategory_id" => "1", "type_id" => "1"}}
  Pipelines: [:browser]
[debug] QUERY OK source="types" db=0.3ms idle=893.5ms
SELECT t0."id", t0."name", t0."inserted_at", t0."updated_at" FROM "types" AS t0 ORDER BY t0."name" []
[debug] QUERY OK source="categories" db=0.2ms idle=894.1ms
SELECT c0."id", c0."name", c0."inserted_at", c0."updated_at" FROM "categories" AS c0 []
[debug] QUERY OK source="subcategories" db=0.2ms idle=894.6ms
SELECT s0."id", s0."name", s0."category_id", s0."inserted_at", s0."updated_at" FROM "subcategories" AS s0 []
[info] Sent 200 in 11ms

IO.inspect in photo controller doesn’t show anything, only in the product’s controller:

%Plug.Conn{
  adapter: {Plug.Cowboy.Conn, :...},
  assigns: %{
    types: [
      %Jordaniva.Inventory.Type{
        __meta__: #Ecto.Schema.Metadata<:loaded, "types">,
        id: 3,
        inserted_at: ~N[2020-08-12 10:46:24],
        name: "Kids",
        updated_at: ~N[2020-08-12 10:46:24]
      },
      %Jordaniva.Inventory.Type{
        __meta__: #Ecto.Schema.Metadata<:loaded, "types">,
        id: 1,
        inserted_at: ~N[2020-08-12 10:46:24],
        name: "Men",
        updated_at: ~N[2020-08-12 10:46:24]
      },
      %Jordaniva.Inventory.Type{
        __meta__: #Ecto.Schema.Metadata<:loaded, "types">,
        id: 2,
        inserted_at: ~N[2020-08-12 10:46:24],
        name: "Women",
        updated_at: ~N[2020-08-12 10:46:24]
      }
    ]
  },
  before_send: [#Function<0.73641281/1 in Plug.CSRFProtection.call/2>,
   #Function<2.102658996/1 in Phoenix.Controller.fetch_flash/2>,
   #Function<0.105793137/1 in Plug.Session.before_send/2>,
   #Function<0.60895335/1 in Plug.Telemetry.call/2>,
   #Function<0.33293990/1 in Phoenix.LiveReloader.before_send_inject_reloader/3>],
  body_params: %{
    "_csrf_token" => "Gg03ClYqHGQcRik4EXU4P0MpDBsJHDchyIEsepMPw0_waXrE2gfBxt_m",
    "photos" => %{
      "category_id" => "1",
      "colour" => "test2",
      "description" => "test2",
      "name" => "test2",
      "photos" => %{
        "0" => %{
          "photos" => [
            %Plug.Upload{
              content_type: "image/jpeg",
              filename: "air_jordan1.jpg",
              path: "/var/folders/1t/q27498654m376jz6fl02fqd80000gn/T//plug-1598/multipart-1598449943-267269738935643-2"
            }
          ]
        }
      },
      "price" => "1111",
      "product_code" => "test2",
      "release_year" => "2222",
      "subcategory_id" => "1",
      "type_id" => "1"
    }
  },
  cookies: %{
    "_jordaniva_key" => "SFMyNTY.g3QAAAABbQAAAAtfY3NyZl90b2tlbm0AAAAYY0RyeTNaUTRrdnZPcC1KenFOallxaGhM.YHsDOvqNysF273T4z74asaQEmNvfLdwOIzE8bjIs3H8"
  },
  halted: false,
  host: "localhost",
  method: "POST",
  owner: #PID<0.1440.0>,
  params: %{
    "_csrf_token" => "Gg03ClYqHGQcRik4EXU4P0MpDBsJHDchyIEsepMPw0_waXrE2gfBxt_m",
    "photos" => %{
      "category_id" => "1",
      "colour" => "test2",
      "description" => "test2",
      "name" => "test2",
      "photos" => %{
        "0" => %{
          "photos" => [
            %Plug.Upload{
              content_type: "image/jpeg",
              filename: "air_jordan1.jpg",
              path: "/var/folders/1t/q27498654m376jz6fl02fqd80000gn/T//plug-1598/multipart-1598449943-267269738935643-2"
            }
          ]
        }
      },
      "price" => "1111",
      "product_code" => "test2",
      "release_year" => "2222",
      "subcategory_id" => "1",
      "type_id" => "1"
    }
  },
  path_info: ["products"],
  path_params: %{},
  port: 4000,
  private: %{
    JordanivaWeb.Router => {[], %{}},
    :phoenix_action => :create,
    :phoenix_controller => JordanivaWeb.ProductController,
    :phoenix_endpoint => JordanivaWeb.Endpoint,
    :phoenix_flash => %{},
    :phoenix_format => "html",
    :phoenix_layout => {JordanivaWeb.LayoutView, :app},
    :phoenix_request_logger => {"request_logger", "request_logger"},
    :phoenix_router => JordanivaWeb.Router,
    :phoenix_view => JordanivaWeb.ProductView,
    :plug_multipart => :done,
    :plug_session => %{"_csrf_token" => "cDry3ZQ4kvvOp-JzqNjYqhhL"},
    :plug_session_fetch => :done
  },
  query_params: %{},
  query_string: "",
  remote_ip: {127, 0, 0, 1},
  req_cookies: %{
    "_jordaniva_key" => "SFMyNTY.g3QAAAABbQAAAAtfY3NyZl90b2tlbm0AAAAYY0RyeTNaUTRrdnZPcC1KenFOallxaGhM.YHsDOvqNysF273T4z74asaQEmNvfLdwOIzE8bjIs3H8"
  },
  req_headers: [
    {"accept",
     "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8"},
    {"accept-encoding", "gzip, deflate"},
    {"accept-language", "pl,en-US;q=0.7,en;q=0.3"},
    {"connection", "keep-alive"},
    {"content-length", "83996"},
    {"content-type",
     "multipart/form-data; boundary=---------------------------12180823701865628394499381825"},
    {"cookie",
     "_jordaniva_key=SFMyNTY.g3QAAAABbQAAAAtfY3NyZl90b2tlbm0AAAAYY0RyeTNaUTRrdnZPcC1KenFOallxaGhM.YHsDOvqNysF273T4z74asaQEmNvfLdwOIzE8bjIs3H8"},
    {"host", "localhost:4000"},
    {"origin", "http://localhost:4000"},
    {"referer", "http://localhost:4000/products"},
    {"upgrade-insecure-requests", "1"},
    {"user-agent",
     "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:79.0) Gecko/20100101 Firefox/79.0"}
  ],
  request_path: "/products",
  resp_body: nil,
  resp_cookies: %{},
  resp_headers: [
    {"cache-control", "max-age=0, private, must-revalidate"},
    {"x-request-id", "Fi7VwTKakmA5xvwAAAYo"},
    {"x-frame-options", "SAMEORIGIN"},
    {"x-xss-protection", "1; mode=block"},
    {"x-content-type-options", "nosniff"},
    {"x-download-options", "noopen"},
    {"x-permitted-cross-domain-policies", "none"},
    {"cross-origin-window-policy", "deny"}
  ],
  scheme: :http,
  script_name: [],
  secret_key_base: :...,
  state: :unset,
  status: nil
}
al2o3cr

al2o3cr

This indicates the request went down the {:error, %Ecto.Changeset{} = changeset} path in the controller. What errors are in that changeset?

al2o3cr

al2o3cr

To clarify - in your photo controller’s create action:

  def create(conn, %{"product_id" => product_id, "photo" => photo_params}) do
    product = Jordaniva.Inventory.get_product!(product_id)
    case Gallery.create_photo(photo_params) do
      {:ok, _photo} ->
        conn
        |> put_flash(:info, "Photo created successfully.")
        |> redirect(to: Routes.photo_path(conn, :index))

      {:error, %Ecto.Changeset{} = changeset} ->
        # The 200 status code in your log shows the code goes down this path
        # Because otherwise the response would be a 302 status code
        # 
        # Let's see what the changeset can tell us:
        IO.inspect(changeset, label: "create_photo changeset")

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

Kapeusz OP

When I put this piece of code in my photo controller create action, there is no output like no parameters are passed. When I IO.inspect product controller create action I got this output.

create_product changeset: #Ecto.Changeset<
  action: :insert,
  changes: %{
    category_id: 1,
    colour: "test4",
    description: "test4",
    name: "test4",
    photos: [
      #Ecto.Changeset<
        action: :insert,
        changes: %{},
        errors: [photo: {"can't be blank", [validation: :required]}],
        data: #Jordaniva.Gallery.Photo<>,
        valid?: false
      >
    ],
    price: #Decimal<444>,
    product_code: "test4",
    release_year: 2000,
    subcategory_id: 1,
    type_id: 1
  },
  errors: [],
  data: #Jordaniva.Inventory.Product<>,
  valid?: false
>

I see the error that errors: [photo: {"can't be blank" but I always try to upload some image.

al2o3cr

al2o3cr

What you’re uploading isn’t showing up where the changeset expects it, so it’s being ignored.

Your parameters are shaped like (from the logs):

%{
  "_csrf_token" => "Ewo7HQVpOGEiIxsdHkk4TUMsPCsQBR0VpNId63iUIUmRndr72bVramuY",
  "photos" => %{
    "category_id" => "1",
    "colour" => "test1",
    "description" => "test1",
    "name" => "test1",
    "photos" => %{
      "0" => %{
        "photos" => [
           %Plug.Upload{content_type: "text/html", filename: "+ud6zQ2o.html", path: "/var/folders/1t/q27498654m376jz6fl02fqd80000gn/T//plug-1598/multipart-1598452423-991068043046336-5"},
           %Plug.Upload{content_type: "image/jpeg", filename: "air_jordan1.jpg", path: "/var/folders/1t/q27498654m376jz6fl02fqd80000gn/T//plug-1598/multipart-1598452423-154324122449949-5"}
        ]
      }
    },
    "price" => "1111",
    "product_code" => "test1",
    "release_year" => "1111",
    "subcategory_id" => "1",
    "type_id" => "1"
  }
}
  • The outer "photos" wrapper is removed by the pattern-match in ProductController.create

  • The next "photos" wrapper is removed by cast_assoc(:photos), which iterates over each element and calls Photo.changeset

  • in this case, there is one element:

%{"photos" => [...array of Plug.Upload structs...]}

But Photo.changeset is expecting a map like:

%{"photo" => %Plug.Upload{}}
Kapeusz

Kapeusz OP

Ohh, I see. Could you tell me/give a hint where I should start with reshaping my code or how do I fix that?

— All posts loaded —

Where Next? Top

Trending in Questions Top

RSP87
I’m working on a project that simulates the bumbl example in the programming phoenix book. It acts almost like an email client. We have a...
New
kszambelanczyk
Hello! Could someone please give me a help/sample code, how to delete a file from s3 using waffle/waffle_ecto from Phoenix app. I creat...
New
RemyXRenard
I’m seeing that a list inside a Kino.DataTable will be interpreted as a charlist, even if the Kino.configure() is set to charlists: :as_l...
New
velrest
So my question is quite simple and i have found no conclusive answer on forum, google or AI. Should we use :erlang.float for Integer to ...
New
samoloth
Hi, I’ve just set up an application with ash_authentication. There is only magic link strategy for now, so there is no confirmation add o...
New
FlyingNoodle
If a change or preparation module uses Ash.Changeset.get_argument/2 or Ash.Query.get_argument/2 (or any of the other get_argument functio...
New
ryanwinchester
apply_graft/2 doesn’t rewrite an add_many sub-workflow’s deps on an add step. Grafted jobs cancel with “upstream job was deleted” Version...
New

Other Trending Topics Top

mudasobwa
I am happy to introduce the very α version of the new programming language compiled to BEAM. Welcome Cure. It has literally three kille...
New
marciok
Hi there! We created Gust: A task orchestrator inspired by Airflow. For those who have never heard about Aiflow, it’s a Python-based wor...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
Dmk
Xamal is a deployment tool for Elixir apps that deploys native releases to bare metal servers over SSH. It’s a port of GitHub - basecamp/...
New
Damirados
Hello everyone. After busy few months I am happy to announce v0.1.0 of Emerge &amp; Solve. They are GUI (Emerge) and State management (S...
New
netoum
Corex is an accessible, unstyled UI component library for Phoenix that integrates Zag.js state machines using Vanilla JavaScript and Live...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews