Skip to content

Commit

Permalink
Export segment contacts
Browse files Browse the repository at this point in the history
  • Loading branch information
katafrakt committed Sep 21, 2023
1 parent 486d92e commit 61b3408
Show file tree
Hide file tree
Showing 4 changed files with 90 additions and 1 deletion.
46 changes: 45 additions & 1 deletion lib/keila_web/controllers/segment_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ defmodule KeilaWeb.SegmentController do
import Ecto.Changeset
import Phoenix.LiveView.Controller

plug :authorize when action not in [:index, :new, :create, :delete]
@csv_export_chunk_size Application.compile_env!(:keila, :csv_export_chunk_size)

plug(:authorize when action not in [:index, :new, :create, :delete])

@spec index(Plug.Conn.t(), map()) :: Plug.Conn.t()
def index(conn, _params) do
Expand Down Expand Up @@ -71,6 +73,48 @@ defmodule KeilaWeb.SegmentController do
|> render("delete.html")
end

@spec contacts_export(Plug.Conn.t(), map()) :: Plug.Conn.t()
def contacts_export(conn, %{"project_id" => project_id, "id" => segment_id}) do
filename = "contacts_#{project_id}_segment_#{segment_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", "Status"]]
|> NimbleCSV.RFC4180.dump_to_iodata()
|> IO.iodata_to_binary()

{:ok, conn} = chunk(conn, header)

args = [
max_rows: @csv_export_chunk_size,
filter: conn.assigns.segment.filter || %{}
]

Keila.Repo.transaction(fn ->
Contacts.stream_project_contacts(project_id, args)
|> 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, contact.status]]
|> 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 @@ -121,6 +121,7 @@ defmodule KeilaWeb.Router do
get "/projects/:project_id/segments/new", SegmentController, :new
post "/projects/:project_id/segments", SegmentController, :create
get "/projects/:project_id/segments/:id", SegmentController, :edit
get "/projects/:project_id/segments/:id/contacts_export", SegmentController, :contacts_export
delete "/projects/:project_id/segments", SegmentController, :delete

get "/projects/:project_id/campaigns", CampaignController, :index
Expand Down
6 changes: 6 additions & 0 deletions lib/keila_web/templates/segment/edit_live.html.heex
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,12 @@
<%= pagination_nav(@contacts, phx_click: "change-page") %>
</div>
</div>
<div>
<a href={Routes.segment_path(@socket, :contacts_export, @current_project.id, @segment.id)} class="button">
<%= render_icon(:document_add) %>
<%= gettext("Export contacts to CSV") %>
</a>
</div>
<% end %>
</div>
</.form>
Expand Down
38 changes: 38 additions & 0 deletions test/keila_web/controllers/segment_controller_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -83,4 +83,42 @@ defmodule KeilaWeb.SegmentControllerTest do
assert segment == Contacts.get_segment(segment.id)
end
end

@tag :segment_controller
test "GET /projects/:p_id/export CSV export contacts in multiple chunks", %{conn: conn} do
{conn, project} = with_login_and_project(conn)

segment =
insert!(:contacts_segment,
project_id: project.id,
filter: %{"email" => %{"$like" => "%keila.io"}}
)

insert!(:contact, project_id: project.id, email: "[email protected]")

contact =
insert!(:contact, project_id: project.id, status: :unreachable, email: "[email protected]")

insert!(:contact, project_id: project.id, email: "[email protected]")
insert!(:contact, project_id: project.id, email: "[email protected]")
insert!(:contact, project_id: project.id, email: "[email protected]")
conn = get(conn, Routes.segment_path(conn, :contacts_export, project.id, segment.id))

assert conn.state == :chunked
rows = String.split(response(conn, 200), "\r\n")
assert length(rows) == 6
assert Enum.at(rows, 1) =~ ~r/,unreachable/

{_, disposition} = List.keyfind(conn.resp_headers, "content-disposition", 0)

assert disposition ==
"attachment; filename=\"contacts_#{project.id}_segment_#{segment.id}.csv\""

assert [
"Email,First name,Last name,Data,Status",
contact_row | _
] = rows

assert contact_row == "[email protected],#{contact.first_name},#{contact.last_name},,unreachable"
end
end

0 comments on commit 61b3408

Please sign in to comment.