Skip to content

Commit

Permalink
Add CSV export for all contacts in the project
Browse files Browse the repository at this point in the history
  • Loading branch information
katafrakt committed Sep 21, 2023
1 parent cda0b13 commit 205778a
Show file tree
Hide file tree
Showing 6 changed files with 98 additions and 13 deletions.
3 changes: 2 additions & 1 deletion config/config.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion config/test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
61 changes: 51 additions & 10 deletions lib/keila_web/controllers/contact_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions lib/keila_web/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 12 additions & 1 deletion lib/keila_web/templates/contact/index.html.heex
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<div class="container flex py-8 sm:py-11 mb-4">
<div class="container flex pt-8 sm:pt-11 mb-4">
<div class="flex-grow gap-4 flex flex-col sm:flex-row sm:items-center">
<h1 class="text-2xl sm:text-3xl text-gray-100">
<%= gettext("Contacts") %>
Expand All @@ -16,6 +16,17 @@
</div>
</div>

<div class="container flex pb-8 sm:pb-11 mb-4">
<div class="flex-grow gap-4 flex flex-col sm:flex-row sm:items-center">
<div class="flex-grow flex flex-row-reverse justify-end gap-4 sm:flex-row">
<a href={Routes.contact_path(@conn, :export, @current_project.id)} class="button">
<%= render_icon(:document_add) %>
<%= gettext("Export to CSV") %>
</a>
</div>
</div>
</div>

<%= if (@contacts_stats.active + @contacts_stats.unsubscribed + @contacts_stats.unreachable) == 0 do %>
<%= render("_empty_state.html", assigns) %>
<% else %>
Expand Down
31 changes: 31 additions & 0 deletions test/keila_web/controllers/contact_controller_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -161,4 +161,35 @@ defmodule KeilaWeb.ContactControllerTest do
assert Keila.Contacts.get_project_contacts(project.id) |> Enum.count() == 201
assert render(lv) =~ "You have successfully imported 201 contacts!"
end

@tag :contact_controller
test "GET /projects/:p_id/export CSV export single contact", %{conn: conn} do
{conn, project} = with_login_and_project(conn)
contact = insert!(:contact, project_id: project.id, data: %{"age" => 42})
conn = get(conn, Routes.contact_path(conn, :export, project.id))
rows = String.split(response(conn, 200), "\r\n")

{_, disposition} = List.keyfind(conn.resp_headers, "content-disposition", 0)
assert disposition == "attachment; filename=\"contacts_#{project.id}.csv\""

assert rows == [
"Email,First name,Last name,Data",
"#{contact.email},#{contact.first_name},#{contact.last_name},\"{\"\"age\"\":42}\"",
""
]
end

@tag :contact_controller
test "GET /projects/:p_id/export CSV export contacts in multiple chunks", %{conn: conn} do
{conn, project} = with_login_and_project(conn)
insert!(:contact, project_id: project.id)
insert!(:contact, project_id: project.id)
insert!(:contact, project_id: project.id)
insert!(:contact, project_id: project.id)
conn = get(conn, Routes.contact_path(conn, :export, project.id))

assert conn.state == :chunked
rows = String.split(response(conn, 200), "\r\n")
assert length(rows) == 6
end
end

0 comments on commit 205778a

Please sign in to comment.