Skip to content

Commit a6c9601

Browse files
committed
Add bulk create backend for local authorities
1 parent bf56ef8 commit a6c9601

File tree

7 files changed

+299
-71
lines changed

7 files changed

+299
-71
lines changed

app/lib/meadow_web/controllers/authority_records_controller.ex

+51
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,57 @@ defmodule MeadowWeb.AuthorityRecordsController do
77

88
plug(:authorize_user)
99

10+
def bulk_create(conn, %{"records" => %Plug.Upload{path: path}}) do
11+
csv_to_maps(path)
12+
|> do_bulk_create(conn)
13+
end
14+
15+
defp do_bulk_create({:error, :bad_format}, conn) do
16+
send_resp(conn, 400, "Bad Request")
17+
end
18+
19+
defp do_bulk_create({:ok, records}, conn) do
20+
results =
21+
records
22+
|> AuthorityRecords.create_authority_records()
23+
|> Enum.map(fn {status, %{id: id, label: label, hint: hint}} ->
24+
["info:nul/#{id}", label, hint, status]
25+
end)
26+
27+
file = "authority_import_#{DateTime.utc_now() |> DateTime.to_unix()}.csv"
28+
29+
conn =
30+
conn
31+
|> put_resp_content_type("text/csv")
32+
|> put_resp_header("content-disposition", ~s[attachment; filename="#{file}"])
33+
|> send_chunked(:ok)
34+
35+
[~w(id label hint status) | results]
36+
|> CSV.dump_to_stream()
37+
|> Stream.each(fn csv_row ->
38+
chunk(conn, csv_row)
39+
end)
40+
|> Stream.run()
41+
42+
conn
43+
end
44+
45+
defp csv_to_maps(file) do
46+
[headers | rows] =
47+
File.stream!(file, [], :line)
48+
|> CSV.parse_stream(skip_headers: false)
49+
|> Enum.to_list()
50+
51+
case Enum.sort(headers) do
52+
~w(hint label) ->
53+
headers = Enum.map(headers, &String.to_atom/1)
54+
{:ok, Enum.map(rows, fn row -> Enum.zip(headers, row) |> Enum.into(%{}) end)}
55+
56+
_ ->
57+
{:error, :bad_format}
58+
end
59+
end
60+
1061
def export(conn, %{"file" => file} = params) do
1162
export(conn, Path.extname(file), params)
1263
end

app/lib/meadow_web/router.ex

+1
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ defmodule MeadowWeb.Router do
5656
pipe_through(:api)
5757

5858
post("/export/:file", MeadowWeb.ExportController, :export)
59+
post("/authority_records/bulk_create", MeadowWeb.AuthorityRecordsController, :bulk_create)
5960
post("/authority_records/:file", MeadowWeb.AuthorityRecordsController, :export)
6061
post("/create_shared_links/:file", MeadowWeb.SharedLinksController, :export)
6162

app/lib/nul/authority_records.ex

+51
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ defmodule NUL.AuthorityRecords do
33
The NUL.AuthorityRecords context.
44
"""
55

6+
# alias Faker.NaiveDateTime
67
alias Meadow.Repo
78
alias NUL.Schemas.AuthorityRecord
89

@@ -90,6 +91,47 @@ defmodule NUL.AuthorityRecords do
9091
|> Repo.insert!()
9192
end
9293

94+
@doc """
95+
Creates many AuthorityRecords at once. Returns a list of [{:created|:duplicate}, %AuthorityRecord{}]
96+
where the record is either the newly created record or the retrieved existing record
97+
"""
98+
def create_authority_records(list_of_attrs) do
99+
inserted_at = NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second)
100+
101+
records =
102+
Enum.map(list_of_attrs, fn attrs ->
103+
Map.merge(attrs, %{
104+
id: Ecto.UUID.generate(),
105+
inserted_at: inserted_at,
106+
updated_at: inserted_at
107+
})
108+
end)
109+
110+
labels = Enum.map(records, & &1.label)
111+
112+
duplicates =
113+
from(ar in AuthorityRecord, where: ar.label in ^labels)
114+
|> Repo.all()
115+
|> indexed_records(:duplicate)
116+
117+
created =
118+
Repo.insert_all(AuthorityRecord, records,
119+
returning: true,
120+
on_conflict: :nothing,
121+
conflict_target: :label
122+
)
123+
|> indexed_records(:created)
124+
125+
results = Enum.into(created ++ duplicates, %{})
126+
Enum.map(labels, &Map.get(results, &1))
127+
end
128+
129+
# def create_authority_records(list_of_attrs) do
130+
# Repo.transaction(fn ->
131+
# {:ok, Enum.map(list_of_attrs, &create_authority_record/1)} |> IO.inspect()
132+
# end)
133+
# end
134+
93135
@doc """
94136
Updates an AuthorityRecord.
95137
@@ -114,4 +156,13 @@ defmodule NUL.AuthorityRecords do
114156
def delete_authority_record(%AuthorityRecord{} = authority_record) do
115157
Repo.delete(authority_record)
116158
end
159+
160+
defp indexed_records({_, records}, status), do: indexed_records(records, status)
161+
162+
defp indexed_records(records, status) do
163+
records
164+
|> Enum.map(fn record ->
165+
{record.label, {status, record}}
166+
end)
167+
end
117168
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
not label,hint
2+
"First Imported Thing",created by test
3+
"Second Imported Thing",created by test
4+
"Third Imported Thing",created by test
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
label,hint
2+
"First Imported Thing",created by test
3+
"Second Imported Thing",created by test
4+
"Third Imported Thing",created by test

app/test/meadow_web/controllers/authority_records_controller_test.exs

+60
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ defmodule MeadowWeb.AuthorityRecordsControllerTest do
22
use Meadow.DataCase
33
use MeadowWeb.ConnCase, async: true
44

5+
alias Meadow.Repo
6+
alias NUL.AuthorityRecords
57
alias NUL.Schemas.AuthorityRecord
68

79
describe "POST /api/authority_records/:filename (failure)" do
@@ -45,6 +47,7 @@ defmodule MeadowWeb.AuthorityRecordsControllerTest do
4547
authority_record_fixture(%{label: "Ver Steeg, Dorothy A."}),
4648
authority_record_fixture(%{label: "Netsch, Walter A."})
4749
]
50+
4851
:ok
4952
end
5053

@@ -62,4 +65,61 @@ defmodule MeadowWeb.AuthorityRecordsControllerTest do
6265
assert response(conn, 200)
6366
end
6467
end
68+
69+
describe "POST /api/authority_records/bulk_create" do
70+
setup %{fixture: fixture} do
71+
upload = %Plug.Upload{
72+
content_type: "text/csv",
73+
filename: "authority_import.csv",
74+
path: fixture
75+
}
76+
77+
{:ok, %{upload: upload}}
78+
end
79+
80+
@tag fixture: "test/fixtures/authority_records/bad_authority_import.csv"
81+
test "bad data", %{conn: conn, upload: upload} do
82+
conn =
83+
conn
84+
|> auth_user(user_fixture("TestAdmins"))
85+
|> post("/api/authority_records/bulk_create", %{records: upload})
86+
87+
assert response(conn, 400)
88+
end
89+
90+
@tag fixture: "test/fixtures/authority_records/good_authority_import.csv"
91+
test "good data", %{conn: conn, upload: upload} do
92+
precount = Meadow.Repo.aggregate(AuthorityRecord, :count)
93+
94+
conn =
95+
conn
96+
|> auth_user(user_fixture("TestAdmins"))
97+
|> post("/api/authority_records/bulk_create", %{records: upload})
98+
99+
assert Meadow.Repo.aggregate(AuthorityRecord, :count) == precount + 3
100+
assert conn.state == :chunked
101+
assert response_content_type(conn, :csv)
102+
assert response(conn, 200)
103+
end
104+
105+
@tag fixture: "test/fixtures/authority_records/good_authority_import.csv"
106+
test "one duplicate entry", %{conn: conn, upload: upload} do
107+
AuthorityRecords.create_authority_record(%{
108+
label: "Second Imported Thing",
109+
hint: "preexisting entry"
110+
})
111+
112+
precount = Meadow.Repo.aggregate(AuthorityRecord, :count)
113+
114+
conn =
115+
conn
116+
|> auth_user(user_fixture("TestAdmins"))
117+
|> post("/api/authority_records/bulk_create", %{records: upload})
118+
119+
assert Meadow.Repo.aggregate(AuthorityRecord, :count) == precount + 2
120+
assert conn.state == :chunked
121+
assert response_content_type(conn, :csv)
122+
assert response(conn, 200)
123+
end
124+
end
65125
end

0 commit comments

Comments
 (0)