diff --git a/config/config.exs b/config/config.exs index 62b2fe07..5d07eac4 100644 --- a/config/config.exs +++ b/config/config.exs @@ -8,7 +8,8 @@ import Config config :keila, - ecto_repos: [Keila.Repo] + ecto_repos: [Keila.Repo], + csv_export_chunk_size: 100 # Configures the endpoint config :keila, KeilaWeb.Endpoint, diff --git a/config/test.exs b/config/test.exs index bca65e44..9b1a634d 100644 --- a/config/test.exs +++ b/config/test.exs @@ -14,7 +14,7 @@ config :keila, Keila.Repo, timeout: 60_000, pool_size: 16 -config :keila, skip_migrations: true +config :keila, skip_migrations: true, csv_export_chunk_size: 3 # We don't run a server during test. If one is required, # you can enable the server option below. diff --git a/lib/keila_web/controllers/contact_controller.ex b/lib/keila_web/controllers/contact_controller.ex index 404501f0..32afd42c 100644 --- a/lib/keila_web/controllers/contact_controller.ex +++ b/lib/keila_web/controllers/contact_controller.ex @@ -4,16 +4,21 @@ defmodule KeilaWeb.ContactController do import Ecto.Changeset alias Keila.Contacts - plug :authorize - when action not in [ - :index, - :index_unsubscribed, - :index_unreachable, - :new, - :post_new, - :delete, - :import - ] + plug( + :authorize + when action not in [ + :index, + :index_unsubscribed, + :index_unreachable, + :new, + :post_new, + :delete, + :import, + :export + ] + ) + + @csv_export_chunk_size Application.compile_env!(:keila, :csv_export_chunk_size) @spec index(Plug.Conn.t(), map()) :: Plug.Conn.t() def index(conn, params) do @@ -152,6 +157,42 @@ defmodule KeilaWeb.ContactController do |> render("edit.html") end + @spec export(Plug.Conn.t(), map()) :: Plug.Conn.t() + def export(conn, %{"project_id" => project_id}) do + filename = "contacts_#{project_id}.csv" + + conn = + conn + |> put_resp_header("content-disposition", "attachment; filename=\"#{filename}\"") + |> put_resp_header("content-type", "text/csv") + |> send_chunked(200) + + header = + [["Email", "First name", "Last name", "Data"]] + |> NimbleCSV.RFC4180.dump_to_iodata() + |> IO.iodata_to_binary() + + {:ok,conn} = chunk(conn, header) + + Keila.Repo.transaction(fn -> + Contacts.stream_project_contacts(project_id, max_rows: @csv_export_chunk_size) + |> Stream.map(fn contact -> + data = if is_nil(contact.data), do: nil, else: Jason.encode!(contact.data) + [[contact.email, contact.first_name, contact.last_name, data]] + |> NimbleCSV.RFC4180.dump_to_iodata() + |> IO.iodata_to_binary() + end) + |> Stream.chunk_every(@csv_export_chunk_size) + |> Enum.reduce_while(conn, fn chunk, conn -> + case Plug.Conn.chunk(conn, chunk) do + {:ok, conn} -> {:cont, conn} + {:error, :closed} -> {:halt, conn} + end + end) + end) + |> then(fn {:ok, conn} -> conn end) + end + defp current_project(conn), do: conn.assigns.current_project defp authorize(conn, _) do diff --git a/lib/keila_web/router.ex b/lib/keila_web/router.ex index 77f4e6e1..e7d2e0cf 100644 --- a/lib/keila_web/router.ex +++ b/lib/keila_web/router.ex @@ -97,6 +97,7 @@ defmodule KeilaWeb.Router do get "/projects/:project_id/contacts/new", ContactController, :new post "/projects/:project_id/contacts/new", ContactController, :post_new get "/projects/:project_id/contacts/import", ContactController, :import + get "/projects/:project_id/contacts/export", ContactController, :export get "/projects/:project_id/contacts/:id", ContactController, :edit put "/projects/:project_id/contacts/:id", ContactController, :post_edit delete "/projects/:project_id/contacts", ContactController, :delete diff --git a/lib/keila_web/templates/contact/index.html.heex b/lib/keila_web/templates/contact/index.html.heex index 3d88ce85..2bd0885d 100644 --- a/lib/keila_web/templates/contact/index.html.heex +++ b/lib/keila_web/templates/contact/index.html.heex @@ -1,4 +1,4 @@ -