From 2fe401b3258e185f21f747a0c437657c11987eaf Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9s=20Vidal?=
Date: Fri, 21 Jun 2024 01:22:16 -0300
Subject: [PATCH 1/8] Allow a single publication to have multiple countries
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Signed-off-by: Andrés Vidal
---
backend/lib/richard_burton/country.ex | 117 ++++-
.../lib/richard_burton/flat_publication.ex | 14 +-
backend/lib/richard_burton/publication.ex | 35 +-
.../lib/richard_burton/publication_codec.ex | 20 +-
.../20240620203719_create_countries.exs | 415 ++++++++++++++++++
.../fixtures/data_correct_with_errors.csv | 2 +-
backend/test/richard_burton/country_test.exs | 312 +++++++++++++
.../richard_burton/flat_publication_test.exs | 24 +-
.../richard_burton/publication_codec_test.exs | 65 ++-
.../richard_burton/publication_index_test.exs | 53 ++-
.../test/richard_burton/publication_test.exs | 26 +-
.../richard_burton/translated_book_test.exs | 2 +-
.../publication_controller_test.exs | 36 +-
frontend/components/DataInput.tsx | 20 +-
frontend/components/Multicombobox.tsx | 61 ++-
frontend/components/PublicationIndexList.tsx | 4 +-
frontend/components/PublicationModal.tsx | 32 +-
frontend/components/TextArrayDataInput.tsx | 14 +-
.../components/TextEnumArrayDataInput.tsx | 52 +++
frontend/modules/publication.ts | 32 +-
20 files changed, 1151 insertions(+), 185 deletions(-)
create mode 100644 backend/priv/repo/migrations/20240620203719_create_countries.exs
create mode 100644 backend/test/richard_burton/country_test.exs
create mode 100644 frontend/components/TextEnumArrayDataInput.tsx
diff --git a/backend/lib/richard_burton/country.ex b/backend/lib/richard_burton/country.ex
index 5b9b90db..0a2ac1b2 100644
--- a/backend/lib/richard_burton/country.ex
+++ b/backend/lib/richard_burton/country.ex
@@ -1,16 +1,123 @@
defmodule RichardBurton.Country do
@moduledoc """
- Utilities for standardized country manipulation
+ Schema for countries
"""
+ use Ecto.Schema
import Ecto.Changeset
- def validate_country(changeset) do
- validate_change(changeset, :country, fn :country, country ->
- if Countries.exists?(:alpha2, country) do
+ alias RichardBurton.Country
+ alias RichardBurton.Repo
+ alias RichardBurton.Publication
+ alias RichardBurton.Util
+
+ @derive {Jason.Encoder, only: [:code]}
+ schema "countries" do
+ field(:code, :string)
+
+ many_to_many(:publications, Publication, join_through: "publication_countries")
+
+ timestamps()
+ end
+
+ @doc false
+ def changeset(country, attrs \\ %{})
+
+ @doc false
+ def changeset(country, attrs = %Country{}) do
+ changeset(country, Map.from_struct(attrs))
+ end
+
+ @doc false
+ def changeset(country, attrs) do
+ country
+ |> cast(attrs, [:code])
+ |> validate_required([:code])
+ |> validate_code()
+ |> unique_constraint(:code)
+ end
+
+ def validate_code(changeset) do
+ validate_change(changeset, :code, fn :code, code ->
+ if Countries.exists?(:alpha2, code) do
[]
else
- [country: {"Invalid ISO-3361-1 alpha2 country code", [validation: :alpha2]}]
+ [code: {"Invalid ISO-3361-1 alpha2 country code: #{code}", [validation: :alpha2]}]
+ end
+ end)
+ end
+
+ def validate_countries(changeset = %Ecto.Changeset{}) do
+ changeset
+ |> validate_required([:countries])
+ |> validate_change(:countries, fn :countries, countries ->
+ case validate_countries(countries) do
+ {:ok} -> []
+ {:error, message} -> [countries: {message, [validation: :alpha2]}]
end
end)
end
+
+ def validate_countries(countries) when is_binary(countries) do
+ invalid =
+ String.split(countries, ",")
+ |> Enum.map(&String.trim/1)
+ |> Enum.map(fn code -> changeset(%Country{}, %{"code" => code}) end)
+ |> Enum.reject(fn cset -> cset.valid? end)
+
+ message = "Invalid countries: #{Enum.map_join(invalid, ", ", &get_change(&1, :code))}"
+
+ case invalid do
+ [] -> {:ok}
+ _ -> {:error, message}
+ end
+ end
+
+ @spec fingerprint(binary() | maybe_improper_list()) :: binary()
+ def fingerprint(countries) when is_binary(countries) do
+ countries
+ |> Publication.Codec.nest_countries()
+ |> Enum.map(fn %{"code" => code} -> %Country{code: code} end)
+ |> fingerprint()
+ end
+
+ def fingerprint(countries) when is_list(countries) do
+ countries
+ |> Enum.map(fn %Country{code: code} -> code end)
+ |> Enum.sort()
+ |> Enum.join()
+ |> Util.create_fingerprint()
+ end
+
+ def maybe_insert!(attrs) do
+ %__MODULE__{}
+ |> changeset(attrs)
+ |> Repo.maybe_insert!([:code])
+ end
+
+ def all do
+ Repo.all(Country)
+ end
+
+ def link(changeset = %{valid?: true}) do
+ countries =
+ changeset
+ |> get_change(:countries)
+ |> Enum.map(&apply_changes/1)
+ |> Enum.map(&maybe_insert!/1)
+
+ put_assoc(changeset, :countries, countries)
+ end
+
+ def link(changeset = %{valid?: false}), do: changeset
+
+ def link_fingerprint(changeset = %Ecto.Changeset{valid?: true}) do
+ countries_fingerprint =
+ changeset
+ |> get_field(:countries)
+ |> fingerprint
+
+ put_change(changeset, :countries_fingerprint, countries_fingerprint)
+ end
+
+ def link_fingerprint(changeset = %Ecto.Changeset{valid?: false}), do: changeset
end
diff --git a/backend/lib/richard_burton/flat_publication.ex b/backend/lib/richard_burton/flat_publication.ex
index de387f29..b635fb1a 100644
--- a/backend/lib/richard_burton/flat_publication.ex
+++ b/backend/lib/richard_burton/flat_publication.ex
@@ -7,7 +7,6 @@ defmodule RichardBurton.FlatPublication do
import Ecto.Query
alias RichardBurton.FlatPublication
- alias RichardBurton.Publication
alias RichardBurton.Repo
alias RichardBurton.TranslatedBook
alias RichardBurton.Validation
@@ -16,7 +15,7 @@ defmodule RichardBurton.FlatPublication do
@external_attributes [
:title,
:year,
- :country,
+ :countries,
:publisher,
:authors,
:original_title,
@@ -27,24 +26,23 @@ defmodule RichardBurton.FlatPublication do
schema "flat_publications" do
field(:title, :string)
field(:year, :integer)
- field(:country, :string)
+ field(:countries, :string)
field(:authors, :string)
field(:publisher, :string)
field(:original_title, :string)
field(:original_authors, :string)
+ field(:countries_fingerprint, :string)
field(:translated_book_fingerprint, :string)
end
@doc false
def changeset(flat_publication, attrs) do
- %Publication{}
- |> Publication.changeset(Publication.Codec.nest(attrs))
-
flat_publication
|> cast(attrs, @external_attributes)
|> validate_required(@external_attributes)
- |> Country.validate_country()
+ |> Country.validate_countries()
+ |> Country.link_fingerprint()
|> TranslatedBook.link_fingerprint()
end
@@ -62,7 +60,7 @@ defmodule RichardBurton.FlatPublication do
[
:title,
:year,
- :country,
+ :countries_fingerprint,
:publisher,
:translated_book_fingerprint
],
diff --git a/backend/lib/richard_burton/publication.ex b/backend/lib/richard_burton/publication.ex
index 429edf33..6ad0844c 100644
--- a/backend/lib/richard_burton/publication.ex
+++ b/backend/lib/richard_burton/publication.ex
@@ -13,18 +13,20 @@ defmodule RichardBurton.Publication do
alias RichardBurton.Validation
alias RichardBurton.Country
- @external_attributes [:country, :publisher, :title, :year, :translated_book]
+ @external_attributes [:countries, :publisher, :title, :year, :translated_book]
@derive {Jason.Encoder, only: @external_attributes}
schema "publications" do
- field(:country, :string)
field(:publisher, :string)
field(:title, :string)
field(:year, :integer)
field(:translated_book_fingerprint, :string)
+ field(:countries_fingerprint, :string)
belongs_to(:translated_book, TranslatedBook)
+ many_to_many(:countries, Country, join_through: "publication_countries")
+
timestamps()
end
@@ -39,15 +41,16 @@ defmodule RichardBurton.Publication do
@doc false
def changeset(publication, attrs) do
publication
- |> cast(attrs, [:title, :year, :country, :publisher])
+ |> cast(attrs, [:title, :year, :publisher])
|> cast_assoc(:translated_book, required: true)
- |> validate_required([:title, :year, :country, :publisher])
- |> Country.validate_country()
- |> TranslatedBook.link_fingerprint()
+ |> cast_assoc(:countries, required: true)
+ |> validate_length(:countries, min: 1)
+ |> validate_required([:title, :year, :publisher])
|> unique_constraint(
- [:title, :year, :country, :publisher, :translated_book_fingerprint],
+ [:title, :year, :publisher, :countries_fingerprint, :translated_book_fingerprint],
name: "publications_composite_key"
)
+ |> link_fingerprints()
end
def all do
@@ -57,13 +60,13 @@ defmodule RichardBurton.Publication do
end
def preload(data) do
- Repo.preload(data, translated_book: [:authors, original_book: [:authors]])
+ Repo.preload(data, [:countries, translated_book: [:authors, original_book: [:authors]]])
end
def insert(attrs) do
%Publication{}
|> changeset(attrs)
- |> TranslatedBook.link()
+ |> link_assocs()
|> Repo.insert()
|> case do
{:ok, publication} ->
@@ -75,7 +78,19 @@ defmodule RichardBurton.Publication do
end
def validate(attrs) do
- Validation.validate(changeset(%Publication{}, attrs), &TranslatedBook.link/1)
+ Validation.validate(changeset(%Publication{}, attrs), &link_assocs/1)
+ end
+
+ defp link_fingerprints(changeset) do
+ changeset
+ |> TranslatedBook.link_fingerprint()
+ |> Country.link_fingerprint()
+ end
+
+ defp link_assocs(changeset) do
+ changeset
+ |> Country.link()
+ |> TranslatedBook.link()
end
def insert_all(attrs_list) do
diff --git a/backend/lib/richard_burton/publication_codec.ex b/backend/lib/richard_burton/publication_codec.ex
index 3a85f9a5..a4a50d5c 100644
--- a/backend/lib/richard_burton/publication_codec.ex
+++ b/backend/lib/richard_burton/publication_codec.ex
@@ -11,7 +11,7 @@ defmodule RichardBurton.Publication.Codec do
@empty_flat_attrs %{
"title" => "",
"year" => "",
- "country" => "",
+ "countries" => "",
"publisher" => "",
"authors" => "",
"original_title" => "",
@@ -21,7 +21,7 @@ defmodule RichardBurton.Publication.Codec do
@csv_headers [
"original_authors",
"year",
- "country",
+ "countries",
"original_title",
"title",
"authors",
@@ -92,6 +92,9 @@ defmodule RichardBurton.Publication.Codec do
defp nest_entry({"original_authors", value}),
do: {"original_authors", nest_authors(value)}
+ defp nest_entry({"countries", value}),
+ do: {"countries", nest_countries(value)}
+
defp nest_entry({key, value}),
do: {key, value}
@@ -99,6 +102,10 @@ defmodule RichardBurton.Publication.Codec do
Enum.map(String.split(authors, ","), &%{"name" => String.trim(&1)})
end
+ def nest_countries(countries) when is_binary(countries) do
+ Enum.map(String.split(countries, ","), &%{"code" => String.trim(&1)})
+ end
+
def flatten(publication = %Publication{}) do
attrs =
publication
@@ -134,6 +141,9 @@ defmodule RichardBurton.Publication.Codec do
defp flatten_entry({"original_authors", value}),
do: {"original_authors", flatten_authors(value)}
+ defp flatten_entry({"countries", value}),
+ do: {"countries", flatten_countries(value)}
+
defp flatten_entry({key, value}),
do: {key, value}
@@ -143,6 +153,12 @@ defmodule RichardBurton.Publication.Codec do
defp flatten_authors(authors), do: authors
+ defp flatten_countries(countries) when is_list(countries) do
+ Enum.map_join(countries, ", ", &(Map.get(&1, "code") || Map.get(&1, :code)))
+ end
+
+ defp flatten_countries(countries), do: countries
+
defp rename_key({"translated_book_authors", v}), do: {"authors", v}
defp rename_key({"translated_book_original_book_title", v}), do: {"original_title", v}
defp rename_key({"translated_book_original_book_authors", v}), do: {"original_authors", v}
diff --git a/backend/priv/repo/migrations/20240620203719_create_countries.exs b/backend/priv/repo/migrations/20240620203719_create_countries.exs
new file mode 100644
index 00000000..8501567c
--- /dev/null
+++ b/backend/priv/repo/migrations/20240620203719_create_countries.exs
@@ -0,0 +1,415 @@
+defmodule RichardBurton.Repo.Migrations.CreateCountries do
+ use Ecto.Migration
+
+ def change do
+ create table(:countries) do
+ add :code, :string
+ timestamps()
+ end
+
+ create unique_index(:countries, :code)
+
+ execute "-- Up
+
+ INSERT INTO countries(
+ code,
+ inserted_at,
+ updated_at
+ )
+ SELECT DISTINCT
+ btrim(unnest(string_to_array(country, ','))) AS country,
+ now(),
+ now()
+ FROM
+ publications;
+ ",
+ "-- Down
+
+ DELETE FROM countries;
+ "
+
+ create table(:publication_countries) do
+ add :publication_id, references(:publications)
+ add :country_id, references(:countries)
+ end
+
+ create unique_index(:publication_countries, [:publication_id, :country_id])
+
+ execute "-- Up
+
+ INSERT INTO publication_countries (
+ publication_id,
+ country_id
+ )
+ SELECT
+ publications.id AS publication_id,
+ countries.id AS country_id
+ FROM
+ publications
+ INNER JOIN
+ countries
+ ON
+ countries.code = ANY(
+ SELECT
+ btrim(unnest(string_to_array(publications.country, ',')))
+ FROM
+ publications AS aux
+ WHERE
+ publications.id = aux.id
+ );
+
+ ",
+ "-- Down
+
+ DELETE FROM publication_countries;
+ "
+
+ alter table("publications") do
+ add :countries_fingerprint, :string
+ end
+
+ execute(
+ "--Up
+ UPDATE publications
+ SET countries_fingerprint = (
+ SELECT
+ encode(sha256(decode(string_agg(countries.code, '' ORDER BY countries.code), 'escape')), 'hex') AS fingerprint
+ FROM
+ publication_countries
+ INNER JOIN
+ countries ON publication_countries.country_id = countries.id
+ WHERE
+ publication_countries.publication_id = publications.id
+ GROUP BY
+ publication_countries.publication_id
+ )
+ ",
+ "--Down
+ "
+ )
+
+ drop(
+ unique_index(
+ :publications,
+ [
+ :title,
+ :year,
+ :country,
+ :publisher,
+ :translated_book_fingerprint
+ ],
+ name: "publications_composite_key"
+ )
+ )
+
+ execute "-- Up
+ ALTER TABLE publications
+ ALTER COLUMN countries_fingerprint SET NOT NULL;
+ ",
+ "-- Down
+
+ ALTER TABLE publications
+ ALTER COLUMN countries_fingerprint DROP NOT NULL;
+ "
+
+ create unique_index(
+ :publications,
+ [
+ :title,
+ :year,
+ :publisher,
+ :translated_book_fingerprint,
+ :countries_fingerprint
+ ],
+ name: "publications_composite_key"
+ )
+
+ execute "--Up
+
+ DROP MATERIALIZED VIEW search_documents;
+ ",
+ "--Down
+
+ CREATE MATERIALIZED VIEW search_documents AS
+ SELECT
+ id,
+ to_tsvector('simple'::regconfig, title) ||
+ to_tsvector('simple'::regconfig, country) ||
+ to_tsvector('simple'::regconfig, publisher) ||
+ to_tsvector('simple'::regconfig, year::text) ||
+ to_tsvector('simple'::regconfig, authors) ||
+ to_tsvector('simple'::regconfig, original_title) ||
+ to_tsvector('simple'::regconfig, original_authors) AS document
+ FROM
+ flat_publications;
+ "
+
+ execute "--Up
+
+ DROP VIEW flat_publications
+ ",
+ "--Down
+ "
+
+ execute "--Up
+
+ CREATE OR REPLACE VIEW flat_publications AS
+ WITH
+ CTE_publications AS (
+ SELECT
+ publications.id AS id,
+ publications.title AS title,
+ publications.year AS year,
+ publications.publisher AS publisher,
+ authors.name AS original_author,
+ translators.name AS translator,
+ original_books.title AS original_title,
+ publications.translated_book_fingerprint AS translated_book_fingerprint,
+ publications.countries_fingerprint AS countries_fingerprint,
+ countries.code AS country
+ FROM
+ translated_books
+ INNER JOIN
+ publications
+ ON publications.translated_book_id = translated_books.id
+ INNER JOIN
+ original_books
+ ON original_books.id = translated_books.original_book_id
+ INNER JOIN
+ original_book_authors
+ ON original_book_authors.original_book_id = original_books.id
+ INNER JOIN
+ authors
+ ON authors.id = original_book_authors.author_id
+ INNER JOIN
+ translated_book_authors
+ ON translated_book_authors.translated_book_id = translated_books.id
+ INNER JOIN
+ authors AS translators
+ ON translators.id = translated_book_authors.author_id
+ INNER JOIN
+ publication_countries
+ ON publication_countries.publication_id = publications.id
+ INNER JOIN
+ countries
+ ON countries.id = publication_countries.country_id
+ ),
+ CTE_authors AS (
+ SELECT id, original_author
+ FROM CTE_publications
+ GROUP BY id, original_author
+ ),
+ CTE_authors_distinct AS (
+ SELECT id, string_agg(original_author, ', ' ORDER BY original_author) AS original_authors
+ FROM CTE_authors
+ GROUP BY id
+ ),
+ CTE_translators AS (
+ SELECT id, translator
+ FROM CTE_publications
+ GROUP BY id, translator
+ ),
+ CTE_translators_distinct AS (
+ SELECT id, string_agg(translator, ', ' ORDER BY translator) AS translators
+ FROM CTE_translators
+ GROUP BY id
+ ),
+ CTE_countries AS (
+ SELECT id, country
+ FROM CTE_publications
+ GROUP BY id, country
+ ),
+ CTE_countries_distinct AS (
+ SELECT id, string_agg(country, ', ' ORDER BY country) AS countries
+ FROM CTE_countries
+ GROUP BY id
+ )
+ SELECT
+ CTE_publications.id AS id,
+ CTE_publications.title AS title,
+ CTE_countries_distinct.countries AS countries,
+ CTE_publications.countries_fingerprint AS countries_fingerprint,
+ CTE_publications.year AS year,
+ CTE_publications.publisher AS publisher,
+ CTE_publications.original_title AS original_title,
+ CTE_authors_distinct.original_authors AS original_authors,
+ CTE_translators_distinct.translators AS authors,
+ CTE_publications.translated_book_fingerprint AS translated_book_fingerprint
+ FROM
+ CTE_publications
+ INNER JOIN
+ CTE_authors_distinct
+ ON CTE_publications.id = CTE_authors_distinct.id
+ INNER JOIN
+ CTE_translators_distinct
+ ON CTE_publications.id = CTE_translators_distinct.id
+ INNER JOIN
+ CTE_countries_distinct
+ ON CTE_publications.id = CTE_countries_distinct.id
+ GROUP BY
+ CTE_publications.id,
+ CTE_publications.title,
+ CTE_countries_distinct.countries,
+ CTE_publications.year,
+ CTE_publications.publisher,
+ CTE_publications.original_title,
+ CTE_authors_distinct.original_authors,
+ CTE_translators_distinct.translators,
+ CTE_publications.translated_book_fingerprint,
+ CTE_publications.countries_fingerprint;
+ ",
+ "-- Down
+
+ CREATE OR REPLACE VIEW flat_publications AS
+ WITH
+ CTE_publications AS (
+ SELECT
+ publications.id AS id,
+ publications.title AS title,
+ publications.country AS country,
+ publications.year AS year,
+ publications.publisher AS publisher,
+ authors.name AS original_author,
+ translators.name AS translator,
+ original_books.title AS original_title,
+ publications.translated_book_fingerprint AS translated_book_fingerprint
+ FROM
+ translated_books
+ INNER JOIN
+ publications
+ ON publications.translated_book_id = translated_books.id
+ INNER JOIN
+ original_books
+ ON original_books.id = translated_books.original_book_id
+ INNER JOIN
+ original_book_authors
+ ON original_book_authors.original_book_id = original_books.id
+ INNER JOIN
+ authors
+ ON authors.id = original_book_authors.author_id
+ INNER JOIN
+ translated_book_authors
+ ON translated_book_authors.translated_book_id = translated_books.id
+ INNER JOIN
+ authors AS translators
+ ON translators.id = translated_book_authors.author_id
+ ),
+ CTE_authors AS (
+ SELECT id, original_author
+ FROM CTE_publications
+ GROUP BY id, original_author
+ ),
+ CTE_authors_distinct AS (
+ SELECT id, string_agg(original_author, ', ' ORDER BY original_author) AS original_authors
+ FROM CTE_authors
+ GROUP BY id
+ ),
+ CTE_translators AS (
+ SELECT id, translator
+ FROM CTE_publications
+ GROUP BY id, translator
+ ),
+ CTE_translators_distinct AS (
+ SELECT id, string_agg(translator, ', ' ORDER BY translator) AS translators
+ FROM CTE_translators
+ GROUP BY id
+ )
+ SELECT
+ CTE_publications.id AS id,
+ CTE_publications.title AS title,
+ CTE_publications.country AS country,
+ CTE_publications.year AS year,
+ CTE_publications.publisher AS publisher,
+ CTE_publications.original_title AS original_title,
+ CTE_authors_distinct.original_authors AS original_authors,
+ CTE_translators_distinct.translators AS authors,
+ CTE_publications.translated_book_fingerprint AS translated_book_fingerprint
+ FROM
+ CTE_publications
+ INNER JOIN
+ CTE_authors_distinct
+ ON CTE_publications.id = CTE_authors_distinct.id
+ INNER JOIN
+ CTE_translators_distinct
+ ON CTE_publications.id = CTE_translators_distinct.id
+ GROUP BY
+ CTE_publications.id,
+ CTE_publications.title,
+ CTE_publications.country,
+ CTE_publications.year,
+ CTE_publications.publisher,
+ CTE_publications.original_title,
+ CTE_authors_distinct.original_authors,
+ CTE_translators_distinct.translators,
+ CTE_publications.translated_book_fingerprint;
+ "
+
+ execute "--Up
+ ",
+ "--Down
+
+ DROP VIEW flat_publications
+ "
+
+ execute "--Up
+
+ CREATE MATERIALIZED VIEW search_documents AS
+ SELECT
+ id,
+ to_tsvector('simple'::regconfig, title) ||
+ to_tsvector('simple'::regconfig, countries) ||
+ to_tsvector('simple'::regconfig, publisher) ||
+ to_tsvector('simple'::regconfig, year::text) ||
+ to_tsvector('simple'::regconfig, authors) ||
+ to_tsvector('simple'::regconfig, original_title) ||
+ to_tsvector('simple'::regconfig, original_authors) AS document
+ FROM
+ flat_publications;
+
+ ",
+ "--Down
+
+ DROP MATERIALIZED VIEW search_documents;
+ "
+
+ execute "-- Up
+ ",
+ "-- Down
+
+ ALTER TABLE publications
+ ALTER COLUMN country SET NOT NULL;
+ "
+
+ execute "-- Up
+ ",
+ "-- Down
+
+ UPDATE publications
+ SET country = (
+ SELECT
+ string_agg(countries.code, ', ' ORDER BY countries.code)
+ FROM
+ countries
+ INNER JOIN
+ publication_countries
+ ON
+ countries.id = publication_countries.country_id
+ WHERE
+ publication_countries.publication_id = publications.id
+ GROUP BY
+ publication_countries.publication_id
+ );
+ "
+
+ execute "-- Up
+
+ ALTER TABLE publications
+ DROP COLUMN country;
+ ",
+ "-- Down
+
+ ALTER TABLE publications
+ ADD country VARCHAR;
+ "
+ end
+end
diff --git a/backend/test/fixtures/data_correct_with_errors.csv b/backend/test/fixtures/data_correct_with_errors.csv
index bafcdc3c..23d1106b 100644
--- a/backend/test/fixtures/data_correct_with_errors.csv
+++ b/backend/test/fixtures/data_correct_with_errors.csv
@@ -1,4 +1,4 @@
José de Alencar;1886;GB;Iracema;Iraçéma the Honey-Lips: A Legend of Brazil;Isabel Burton, Richard Burton;Bickers & Son
-José de Alencar;1922;US;Ubirajara;Ubirajara: A Legend of the Tupy Indians;J. T. W. Sadler;Ronald Massey
+José de Alencar;1922;US, GB;Ubirajara;Ubirajara: A Legend of the Tupy Indians;J. T. W. Sadler;Ronald Massey
José de Alencar;AAAA;GB;Iracema;;;Bickers & Son
;;;;Ubirajara: A Legend of the Tupy Indians;J. T. W. Sadler;
\ No newline at end of file
diff --git a/backend/test/richard_burton/country_test.exs b/backend/test/richard_burton/country_test.exs
new file mode 100644
index 00000000..a9df5e04
--- /dev/null
+++ b/backend/test/richard_burton/country_test.exs
@@ -0,0 +1,312 @@
+defmodule RichardBurton.CountryTest do
+ @moduledoc """
+ Tests for the Country schema
+ """
+ use RichardBurton.DataCase
+
+ alias RichardBurton.Country
+ alias RichardBurton.Validation
+ alias RichardBurton.Util
+
+ defmodule WithStringCountries do
+ use Ecto.Schema
+ import Ecto.Changeset
+
+ schema "with_countries" do
+ field(:countries, :string)
+ end
+
+ def changeset(attrs = %{}) do
+ %WithStringCountries{}
+ |> cast(attrs, [:countries])
+ end
+ end
+
+ defmodule WithManyCountries do
+ use Ecto.Schema
+ import Ecto.Changeset
+
+ schema "with_many_countries" do
+ field(:countries_fingerprint, :string)
+ has_many :countries, Country
+ end
+
+ def changeset(attrs) do
+ %WithManyCountries{} |> cast(attrs, []) |> cast_assoc(:countries)
+ end
+ end
+
+ @valid_attrs %{
+ "code" => "GB"
+ }
+
+ defp changeset(attrs = %{}) do
+ Country.changeset(%Country{}, attrs)
+ end
+
+ defp change_valid(attrs = %{}) do
+ changeset(Util.deep_merge_maps(@valid_attrs, attrs))
+ end
+
+ defp insert(attrs) do
+ %Country{} |> Country.changeset(attrs) |> Repo.insert()
+ end
+
+ defp insert!(attrs) do
+ attrs |> changeset() |> Repo.insert!()
+ end
+
+ defp get_countries(changeset = %Ecto.Changeset{}) do
+ changeset
+ |> get_change(:countries)
+ |> Enum.map(&apply_changes/1)
+ end
+
+ defp get_fingerprint(changeset = %Ecto.Changeset{}) do
+ get_change(changeset, :countries_fingerprint)
+ end
+
+ describe "changeset/2" do
+ test "when valid attributes are provided, is valid" do
+ assert changeset(@valid_attrs).valid?
+ end
+
+ test "when code is blank, is invalid" do
+ refute change_valid(%{"code" => ""}).valid?
+ end
+
+ test "when code is nil, is invalid" do
+ refute change_valid(%{"code" => nil}).valid?
+ end
+
+ test "when code is valid alpha3 code, is invalid" do
+ refute change_valid(%{"code" => "USA"}).valid?
+ end
+
+ test "when code is invalid 3 digit code, is invalid" do
+ refute change_valid(%{"code" => "EUA"}).valid?
+ end
+
+ test "when code is invalid 2 digit code, is invalid" do
+ refute change_valid(%{"code" => "XX"}).valid?
+ end
+
+ test "when a country with the provided attributes already exists, is invalid" do
+ {:ok, _} = insert(@valid_attrs)
+ {:error, changeset} = insert(@valid_attrs)
+
+ refute changeset.valid?
+ assert :conflict == Validation.get_errors(changeset)
+ end
+ end
+
+ describe "validate_countries/1" do
+ test "when countries is valid alpha2 code, is valid" do
+ %Ecto.Changeset{errors: errors, valid?: valid} =
+ %{"countries" => "GB"}
+ |> WithStringCountries.changeset()
+ |> Country.validate_countries()
+
+ assert valid
+ assert errors == []
+ end
+
+ test "when countries is multiple, comma separated, valid codes, is valid" do
+ %Ecto.Changeset{errors: errors, valid?: valid} =
+ %{"countries" => "GB,US"}
+ |> WithStringCountries.changeset()
+ |> Country.validate_countries()
+
+ assert valid
+ assert errors == []
+ end
+
+ test "when countries is blank, is invalid" do
+ %Ecto.Changeset{errors: errors, valid?: valid} =
+ %{"countries" => ""}
+ |> WithStringCountries.changeset()
+ |> Country.validate_countries()
+
+ refute valid
+ assert errors == [countries: {"can't be blank", [validation: :required]}]
+ end
+
+ test "when countries is nil, is invalid" do
+ %Ecto.Changeset{errors: errors, valid?: valid} =
+ %{"countries" => nil}
+ |> WithStringCountries.changeset()
+ |> Country.validate_countries()
+
+ refute valid
+ assert errors == [countries: {"can't be blank", [validation: :required]}]
+ end
+
+ test "when countries is valid alpha3 code, is invalid" do
+ %Ecto.Changeset{errors: errors, valid?: valid} =
+ %{"countries" => "USA"}
+ |> WithStringCountries.changeset()
+ |> Country.validate_countries()
+
+ refute valid
+ assert errors == [countries: {"Invalid countries: USA", [validation: :alpha2]}]
+ end
+
+ test "when countries is invalid 3 digit code, is invalid" do
+ %Ecto.Changeset{errors: errors, valid?: valid} =
+ %{"countries" => "EUA"}
+ |> WithStringCountries.changeset()
+ |> Country.validate_countries()
+
+ refute valid
+ assert errors == [countries: {"Invalid countries: EUA", [validation: :alpha2]}]
+ end
+
+ test "when countries is multiple, comma separated, invalid codes, is invalid" do
+ %Ecto.Changeset{errors: errors, valid?: valid} =
+ %{"countries" => "USA,GBR"}
+ |> WithStringCountries.changeset()
+ |> Country.validate_countries()
+
+ refute valid
+ assert errors == [countries: {"Invalid countries: USA, GBR", [validation: :alpha2]}]
+ end
+
+ test "when countries has at least one invalid code, is invalid" do
+ %Ecto.Changeset{errors: errors, valid?: valid} =
+ %{"countries" => "USA,GB"}
+ |> WithStringCountries.changeset()
+ |> Country.validate_countries()
+
+ refute valid
+ assert errors == [countries: {"Invalid countries: USA", [validation: :alpha2]}]
+ end
+
+ test "when countries is invalid 2 digit code, is invalid" do
+ %Ecto.Changeset{errors: errors, valid?: valid} =
+ %{"countries" => "XX"}
+ |> WithStringCountries.changeset()
+ |> Country.validate_countries()
+
+ refute valid
+ assert errors == [countries: {"Invalid countries: XX", [validation: :alpha2]}]
+ end
+ end
+
+ describe "maybe_insert/1" do
+ test "when there is no country with the provided name, inserts it" do
+ country = Country.maybe_insert!(@valid_attrs)
+
+ assert [country] == Country.all()
+ end
+
+ test "when there is a country with the provided name, returns the pre-existent one" do
+ insert(@valid_attrs)
+ assert [preexistent_country] = Country.all()
+
+ country = Country.maybe_insert!(@valid_attrs)
+
+ assert preexistent_country == country
+ assert [country] == Country.all()
+ end
+ end
+
+ describe "fingerprint/1" do
+ test "given two different lists of countries, generates different fingerprints" do
+ countries1 = [%Country{code: "GB"}, %Country{code: "US"}]
+ countries2 = [%Country{code: "US"}, %Country{code: "IE"}]
+
+ refute Country.fingerprint(countries1) == Country.fingerprint(countries2)
+ end
+
+ test "given two lists of countries with the same names, generates the same fingerprints" do
+ countries1 = [%Country{code: "GB"}, %Country{code: "US"}]
+ countries2 = [%Country{code: "GB"}, %Country{code: "US"}]
+
+ assert Country.fingerprint(countries1) == Country.fingerprint(countries2)
+ end
+
+ test "given two lists of countries with the same and different order, generates the same fingerprints" do
+ countries1 = [%Country{code: "GB"}, %Country{code: "US"}]
+ countries2 = [%Country{code: "US"}, %Country{code: "GB"}]
+
+ assert Country.fingerprint(countries1) == Country.fingerprint(countries2)
+ end
+ end
+
+ describe "link/1" do
+ test "links existing countries to changeset" do
+ attrs = %{
+ "countries" => [
+ %{"code" => "GB"},
+ %{"code" => "US"}
+ ]
+ }
+
+ countries = Enum.map(attrs["countries"], &insert!/1)
+
+ changeset =
+ attrs
+ |> WithManyCountries.changeset()
+ |> Country.link()
+
+ assert changeset.valid?
+ assert countries == get_countries(changeset)
+ end
+
+ test "links non-existing countries to changeset, inserting them" do
+ attrs = %{
+ "countries" => [
+ %{"code" => "GB"},
+ %{"code" => "US"}
+ ]
+ }
+
+ changeset =
+ attrs
+ |> WithManyCountries.changeset()
+ |> Country.link()
+
+ assert changeset.valid?
+ assert Country.all() == get_countries(changeset)
+ end
+
+ test "has no side effects when changeset is invalid" do
+ changeset =
+ %{"countries" => [%{}]}
+ |> WithManyCountries.changeset()
+ |> Country.link()
+
+ refute changeset.valid?
+ assert Enum.empty?(Country.all())
+ end
+ end
+
+ describe "link_fingerprint/1" do
+ test "links fingerprint using country names to changeset" do
+ attrs = %{
+ "countries" => [
+ %{"code" => "GB"},
+ %{"code" => "US"}
+ ]
+ }
+
+ changeset =
+ attrs
+ |> WithManyCountries.changeset()
+ |> Country.link_fingerprint()
+
+ assert changeset.valid?
+ assert Country.fingerprint(get_countries(changeset)) == get_fingerprint(changeset)
+ end
+
+ test "does not link fingerprint to invalid changeset" do
+ changeset =
+ %{"countries" => [%{}]}
+ |> WithManyCountries.changeset()
+ |> Country.link_fingerprint()
+
+ refute changeset.valid?
+ assert is_nil(get_fingerprint(changeset))
+ end
+ end
+end
diff --git a/backend/test/richard_burton/flat_publication_test.exs b/backend/test/richard_burton/flat_publication_test.exs
index 4041a505..d723dddb 100644
--- a/backend/test/richard_burton/flat_publication_test.exs
+++ b/backend/test/richard_burton/flat_publication_test.exs
@@ -11,7 +11,7 @@ defmodule RichardBurton.FlatPublicationTest do
@valid_attrs %{
"title" => "Manuel de Moraes: A Chronicle of the Seventeenth Century",
- "country" => "GB",
+ "countries" => "GB",
"year" => 1886,
"publisher" => "Bickers & Son",
"authors" => "Richard Burton, Isabel Burton",
@@ -33,7 +33,7 @@ defmodule RichardBurton.FlatPublicationTest do
@empty_attrs_error_map %{
title: :required,
- country: :required,
+ countries: :required,
year: :required,
publisher: :required,
authors: :required,
@@ -66,26 +66,6 @@ defmodule RichardBurton.FlatPublicationTest do
refute change_valid(%{"title" => nil}).valid?
end
- test "when country is blank, is invalid" do
- refute change_valid(%{"country" => ""}).valid?
- end
-
- test "when country is nil, is invalid" do
- refute change_valid(%{"country" => nil}).valid?
- end
-
- test "when country is valid alpha3 code, is invalid" do
- refute change_valid(%{"country" => "USA"}).valid?
- end
-
- test "when country is invalid 3 digit code, is invalid" do
- refute change_valid(%{"country" => "EUA"}).valid?
- end
-
- test "when country is invalid 2 digit code, is invalid" do
- refute change_valid(%{"country" => "XX"}).valid?
- end
-
test "when publisher is blank, is invalid" do
refute change_valid(%{"publisher" => ""}).valid?
end
diff --git a/backend/test/richard_burton/publication_codec_test.exs b/backend/test/richard_burton/publication_codec_test.exs
index 59851aa1..014eba8c 100644
--- a/backend/test/richard_burton/publication_codec_test.exs
+++ b/backend/test/richard_burton/publication_codec_test.exs
@@ -6,6 +6,7 @@ defmodule RichardBurton.Publication.CodecTest do
use RichardBurton.DataCase
alias RichardBurton.Author
+ alias RichardBurton.Country
alias RichardBurton.Publication
alias RichardBurton.FlatPublication
alias RichardBurton.TranslatedBook
@@ -18,7 +19,7 @@ defmodule RichardBurton.Publication.CodecTest do
output = [
%{
"authors" => "Isabel Burton, Richard Burton",
- "country" => "GB",
+ "countries" => "GB",
"original_authors" => "José de Alencar",
"original_title" => "Iracema",
"publisher" => "Bickers & Son",
@@ -27,7 +28,7 @@ defmodule RichardBurton.Publication.CodecTest do
},
%{
"authors" => "J. T. W. Sadler",
- "country" => "US",
+ "countries" => "US, GB",
"original_authors" => "José de Alencar",
"original_title" => "Ubirajara",
"publisher" => "Ronald Massey",
@@ -36,7 +37,7 @@ defmodule RichardBurton.Publication.CodecTest do
},
%{
"authors" => "",
- "country" => "GB",
+ "countries" => "GB",
"original_authors" => "José de Alencar",
"original_title" => "Iracema",
"publisher" => "Bickers & Son",
@@ -45,7 +46,7 @@ defmodule RichardBurton.Publication.CodecTest do
},
%{
"authors" => "J. T. W. Sadler",
- "country" => "",
+ "countries" => "",
"original_authors" => "",
"original_title" => "",
"publisher" => "",
@@ -65,7 +66,7 @@ defmodule RichardBurton.Publication.CodecTest do
output = [
%{
"authors" => "Isabel Burton",
- "country" => "GB",
+ "countries" => "GB",
"original_authors" => "José de Alencar",
"original_title" => "Iracema",
"publisher" => "",
@@ -74,7 +75,7 @@ defmodule RichardBurton.Publication.CodecTest do
},
%{
"authors" => "Ronald Massey",
- "country" => "Ubirajara",
+ "countries" => "Ubirajara",
"original_authors" => "",
"original_title" => "Ubirajara: A Legend of the Tupy Indians",
"publisher" => "",
@@ -83,7 +84,7 @@ defmodule RichardBurton.Publication.CodecTest do
},
%{
"authors" => "",
- "country" => "",
+ "countries" => "",
"original_authors" =>
"José de Alencar,1886,GB,Iracema,Iraçéma the Honey-Lips: A Legend of Brazil,Isabel Burton,Bickers & Son",
"original_title" => "",
@@ -111,7 +112,7 @@ defmodule RichardBurton.Publication.CodecTest do
@output %{
"title" => "Iraçéma the Honey-Lips: A Legend of Brazil",
"year" => "1886",
- "country" => "GB",
+ "countries" => "GB",
"publisher" => "Bickers & Son",
"authors" => "Isabel Burton",
"original_authors" => "José de Alencar",
@@ -121,7 +122,8 @@ defmodule RichardBurton.Publication.CodecTest do
@output_struct %FlatPublication{
title: "Iraçéma the Honey-Lips: A Legend of Brazil",
year: 1886,
- country: "GB",
+ countries: "GB",
+ countries_fingerprint: "B4043B0B8297E379BC559AB33B6AE9C7A9B4EF6519D3BAEE53270F0C0DD3D960",
publisher: "Bickers & Son",
authors: "Isabel Burton",
original_authors: "José de Alencar",
@@ -134,7 +136,7 @@ defmodule RichardBurton.Publication.CodecTest do
input = %{
"title" => "Iraçéma the Honey-Lips: A Legend of Brazil",
"year" => "1886",
- "country" => "GB",
+ "countries" => "GB",
"publisher" => "Bickers & Son",
"translated_book" => %{
"authors" => [
@@ -156,7 +158,7 @@ defmodule RichardBurton.Publication.CodecTest do
input = %{
title: "Iraçéma the Honey-Lips: A Legend of Brazil",
year: "1886",
- country: "GB",
+ countries: [%Country{code: "GB"}],
publisher: "Bickers & Son",
translated_book: %{
authors: [
@@ -178,16 +180,12 @@ defmodule RichardBurton.Publication.CodecTest do
input = %Publication{
title: "Iraçéma the Honey-Lips: A Legend of Brazil",
year: "1886",
- country: "GB",
+ countries: [%Country{code: "GB"}],
publisher: "Bickers & Son",
translated_book: %TranslatedBook{
- authors: [
- %Author{name: "Isabel Burton"}
- ],
+ authors: [%Author{name: "Isabel Burton"}],
original_book: %OriginalBook{
- authors: [
- %Author{name: "José de Alencar"}
- ],
+ authors: [%Author{name: "José de Alencar"}],
title: "Iracema"
}
}
@@ -201,7 +199,7 @@ defmodule RichardBurton.Publication.CodecTest do
%{
"title" => "Iraçéma the Honey-Lips: A Legend of Brazil",
"year" => "1886",
- "country" => "GB",
+ "countries" => [%{"code" => "GB"}],
"publisher" => "Bickers & Son",
"translated_book" => %{
"authors" => [
@@ -218,7 +216,7 @@ defmodule RichardBurton.Publication.CodecTest do
%{
title: "Iraçéma the Honey-Lips: A Legend of Brazil",
year: "1886",
- country: "GB",
+ countries: [%{code: "GB"}],
publisher: "Bickers & Son",
translated_book: %{
authors: [
@@ -235,7 +233,7 @@ defmodule RichardBurton.Publication.CodecTest do
%Publication{
title: "Iraçéma the Honey-Lips: A Legend of Brazil",
year: "1886",
- country: "GB",
+ countries: [%Country{code: "GB"}],
publisher: "Bickers & Son",
translated_book: %TranslatedBook{
authors: [
@@ -259,7 +257,7 @@ defmodule RichardBurton.Publication.CodecTest do
@output_publication %{
"title" => "Ubirajara: A Legend of the Tupy Indians",
"year" => "",
- "country" => "",
+ "countries" => "",
"publisher" => "",
"authors" => "J. T. W. Sadler",
"original_authors" => "",
@@ -270,7 +268,7 @@ defmodule RichardBurton.Publication.CodecTest do
input_publication = %{
"title" => "Ubirajara: A Legend of the Tupy Indians",
"year" => "",
- "country" => "",
+ "countries" => "",
"publisher" => "",
"translated_book" => %{
"authors" => [
@@ -290,7 +288,7 @@ defmodule RichardBurton.Publication.CodecTest do
publication: input_publication,
errors: %{
year: "required",
- country: "required",
+ countries: "required",
publisher: "required",
translated_book: %{
original_book: %{
@@ -315,7 +313,7 @@ defmodule RichardBurton.Publication.CodecTest do
"publication" => @output_publication,
"errors" => %{
"year" => "required",
- "country" => "required",
+ "countries" => "required",
"publisher" => "required",
"original_authors" => "required",
"original_title" => "required"
@@ -339,7 +337,7 @@ defmodule RichardBurton.Publication.CodecTest do
@output %{
"title" => "Iraçéma the Honey-Lips: A Legend of Brazil",
"year" => "1886",
- "country" => "GB",
+ "countries" => [%{"code" => "GB"}],
"publisher" => "Bickers & Son",
"translated_book" => %{
"authors" => [
@@ -357,7 +355,8 @@ defmodule RichardBurton.Publication.CodecTest do
@output_struct %Publication{
title: "Iraçéma the Honey-Lips: A Legend of Brazil",
year: 1886,
- country: "GB",
+ countries: [%Country{code: "GB"}],
+ countries_fingerprint: "B4043B0B8297E379BC559AB33B6AE9C7A9B4EF6519D3BAEE53270F0C0DD3D960",
publisher: "Bickers & Son",
translated_book: %TranslatedBook{
authors: [
@@ -382,7 +381,7 @@ defmodule RichardBurton.Publication.CodecTest do
input = %{
"title" => "Iraçéma the Honey-Lips: A Legend of Brazil",
"year" => "1886",
- "country" => "GB",
+ "countries" => "GB",
"publisher" => "Bickers & Son",
"authors" => "Isabel Burton",
"original_authors" => "José de Alencar",
@@ -396,7 +395,7 @@ defmodule RichardBurton.Publication.CodecTest do
input = %{
title: "Iraçéma the Honey-Lips: A Legend of Brazil",
year: "1886",
- country: "GB",
+ countries: "GB",
publisher: "Bickers & Son",
authors: "Isabel Burton",
original_authors: "José de Alencar",
@@ -410,7 +409,7 @@ defmodule RichardBurton.Publication.CodecTest do
input = %FlatPublication{
title: "Iraçéma the Honey-Lips: A Legend of Brazil",
year: "1886",
- country: "GB",
+ countries: "GB",
publisher: "Bickers & Son",
authors: "Isabel Burton",
original_authors: "José de Alencar",
@@ -425,7 +424,7 @@ defmodule RichardBurton.Publication.CodecTest do
%{
"title" => "Iraçéma the Honey-Lips: A Legend of Brazil",
"year" => "1886",
- "country" => "GB",
+ "countries" => "GB",
"publisher" => "Bickers & Son",
"authors" => "Isabel Burton",
"original_authors" => "José de Alencar",
@@ -434,7 +433,7 @@ defmodule RichardBurton.Publication.CodecTest do
%{
title: "Iraçéma the Honey-Lips: A Legend of Brazil",
year: "1886",
- country: "GB",
+ countries: "GB",
publisher: "Bickers & Son",
authors: "Isabel Burton",
original_authors: "José de Alencar",
@@ -443,7 +442,7 @@ defmodule RichardBurton.Publication.CodecTest do
%FlatPublication{
title: "Iraçéma the Honey-Lips: A Legend of Brazil",
year: "1886",
- country: "GB",
+ countries: "GB",
publisher: "Bickers & Son",
authors: "Isabel Burton",
original_authors: "José de Alencar",
diff --git a/backend/test/richard_burton/publication_index_test.exs b/backend/test/richard_burton/publication_index_test.exs
index 184c9b01..1bfe4f85 100644
--- a/backend/test/richard_burton/publication_index_test.exs
+++ b/backend/test/richard_burton/publication_index_test.exs
@@ -12,7 +12,8 @@ defmodule RichardBurton.Publication.IndexTest do
@publications [
%FlatPublication{
authors: "Arthur Brakel",
- country: "CA",
+ countries: "CA",
+ countries_fingerprint: "4B650E5C4785025DEE7BD65E3C5C527356717D7A1C0BFEF5B4ADA8CA1E9CBE17",
original_authors: "Cyro dos Anjos",
original_title: "O amanuense Belmiro",
publisher: "Fairleigh Dickinson University Press",
@@ -21,7 +22,8 @@ defmodule RichardBurton.Publication.IndexTest do
},
%FlatPublication{
authors: "Arthur Brakel",
- country: "GB",
+ countries: "GB",
+ countries_fingerprint: "B4043B0B8297E379BC559AB33B6AE9C7A9B4EF6519D3BAEE53270F0C0DD3D960",
original_authors: "Cyro dos Anjos",
original_title: "O amanuense Belmiro",
publisher: "Associated University Presses",
@@ -30,7 +32,8 @@ defmodule RichardBurton.Publication.IndexTest do
},
%FlatPublication{
authors: "Dorothy Scott Loos",
- country: "US",
+ countries: "GB, US",
+ countries_fingerprint: "F060274D35CC0709781F13A9331376B035C9A04546FE43381BC5749F1362C8BF",
original_authors: "Rachel de Queiroz",
original_title: "Dora Doralina",
publisher: "Dutton",
@@ -39,7 +42,8 @@ defmodule RichardBurton.Publication.IndexTest do
},
%FlatPublication{
authors: "E. Percy Ellis",
- country: "BR",
+ countries: "BR",
+ countries_fingerprint: "BBAF8352442730E92C16C5EA6B0FF7CC595C24E02D8E8BFC5FEA5A4E0BB0B46B",
original_authors: "Machado de Assis",
original_title: "Memórias póstumas de Brás Cubas",
publisher: "Instituto Nacional do Livro",
@@ -48,7 +52,8 @@ defmodule RichardBurton.Publication.IndexTest do
},
%FlatPublication{
authors: "Fred P. Ellison",
- country: "US",
+ countries: "US",
+ countries_fingerprint: "9B202ECBC6D45C6D8901D989A918878397A3EB9D00E8F48022FC051B19D21A1D",
original_authors: "Rachel de Queiroz",
original_title: "As três Marias",
publisher: "University of Texas Press",
@@ -57,7 +62,8 @@ defmodule RichardBurton.Publication.IndexTest do
},
%FlatPublication{
authors: "Gregory Rabassa",
- country: "US",
+ countries: "US",
+ countries_fingerprint: "9B202ECBC6D45C6D8901D989A918878397A3EB9D00E8F48022FC051B19D21A1D",
original_authors: "Machado de Assis",
original_title: "Memórias póstumas de Brás Cubas",
publisher: "Oxford University Press",
@@ -66,7 +72,8 @@ defmodule RichardBurton.Publication.IndexTest do
},
%FlatPublication{
authors: "Jean Neel Karnoff",
- country: "GB",
+ countries: "GB",
+ countries_fingerprint: "B4043B0B8297E379BC559AB33B6AE9C7A9B4EF6519D3BAEE53270F0C0DD3D960",
original_authors: "Erico Verissimo",
original_title: "Olhai os lírios do campo",
publisher: "Greenwood",
@@ -75,7 +82,8 @@ defmodule RichardBurton.Publication.IndexTest do
},
%FlatPublication{
authors: "L. C. Kaplan",
- country: "US",
+ countries: "US",
+ countries_fingerprint: "9B202ECBC6D45C6D8901D989A918878397A3EB9D00E8F48022FC051B19D21A1D",
original_authors: "Graciliano Ramos",
original_title: "Angústia",
publisher: "Alfred A. Knopf",
@@ -84,7 +92,8 @@ defmodule RichardBurton.Publication.IndexTest do
},
%FlatPublication{
authors: "Linton Lemos Barrett",
- country: "GB",
+ countries: "GB",
+ countries_fingerprint: "B4043B0B8297E379BC559AB33B6AE9C7A9B4EF6519D3BAEE53270F0C0DD3D960",
original_authors: "Erico Verissimo",
original_title: "O tempo e o vento",
publisher: "Arco Publications",
@@ -93,7 +102,8 @@ defmodule RichardBurton.Publication.IndexTest do
},
%FlatPublication{
authors: "Linton Lemos Barrett",
- country: "GB",
+ countries: "GB",
+ countries_fingerprint: "B4043B0B8297E379BC559AB33B6AE9C7A9B4EF6519D3BAEE53270F0C0DD3D960",
original_authors: "Erico Verissimo",
original_title: "Noite",
publisher: "Arco Publications",
@@ -102,7 +112,8 @@ defmodule RichardBurton.Publication.IndexTest do
},
%FlatPublication{
authors: "Linton Lemos Barrett",
- country: "US",
+ countries: "US",
+ countries_fingerprint: "9B202ECBC6D45C6D8901D989A918878397A3EB9D00E8F48022FC051B19D21A1D",
original_authors: "Erico Verissimo",
original_title: "O tempo e o vento",
publisher: "Macmillan",
@@ -111,7 +122,8 @@ defmodule RichardBurton.Publication.IndexTest do
},
%FlatPublication{
authors: "Linton Lemos Barrett",
- country: "US",
+ countries: "US",
+ countries_fingerprint: "9B202ECBC6D45C6D8901D989A918878397A3EB9D00E8F48022FC051B19D21A1D",
original_authors: "Erico Verissimo",
original_title: "Noite",
publisher: "Macmillan",
@@ -120,7 +132,8 @@ defmodule RichardBurton.Publication.IndexTest do
},
%FlatPublication{
authors: "Linton Lemos Barrett, Marie Barrett",
- country: "US",
+ countries: "US",
+ countries_fingerprint: "9B202ECBC6D45C6D8901D989A918878397A3EB9D00E8F48022FC051B19D21A1D",
original_authors: "Erico Verissimo",
original_title: "O senhor embaixador",
publisher: "Macmillan",
@@ -129,7 +142,8 @@ defmodule RichardBurton.Publication.IndexTest do
},
%FlatPublication{
authors: "Margaret Richardson Hollingsworth",
- country: "US",
+ countries: "US",
+ countries_fingerprint: "9B202ECBC6D45C6D8901D989A918878397A3EB9D00E8F48022FC051B19D21A1D",
original_authors: "Mário de Andrade",
original_title: "Amar verbo intransitivo",
publisher: "MacCaulay",
@@ -138,7 +152,8 @@ defmodule RichardBurton.Publication.IndexTest do
},
%FlatPublication{
authors: "Thomas Colchie",
- country: "US",
+ countries: "US",
+ countries_fingerprint: "9B202ECBC6D45C6D8901D989A918878397A3EB9D00E8F48022FC051B19D21A1D",
original_authors: "Graciliano Ramos",
original_title: "Memórias do cárcere",
publisher: "Evans",
@@ -147,7 +162,8 @@ defmodule RichardBurton.Publication.IndexTest do
},
%FlatPublication{
authors: "William L. Grossman",
- country: "GB",
+ countries: "GB",
+ countries_fingerprint: "B4043B0B8297E379BC559AB33B6AE9C7A9B4EF6519D3BAEE53270F0C0DD3D960",
original_authors: "Machado de Assis",
original_title: "Memórias póstumas de Brás Cubas",
publisher: "W.H. Allen",
@@ -156,7 +172,8 @@ defmodule RichardBurton.Publication.IndexTest do
},
%FlatPublication{
authors: "William L. Grossman",
- country: "US",
+ countries: "US",
+ countries_fingerprint: "9B202ECBC6D45C6D8901D989A918878397A3EB9D00E8F48022FC051B19D21A1D",
original_authors: "Machado de Assis",
original_title: "Memórias póstumas de Brás Cubas",
publisher: "Noonday Press",
@@ -331,7 +348,7 @@ defmodule RichardBurton.Publication.IndexTest do
assert_search_results(
publications,
expect: [
- country: expected_countries
+ countries: expected_countries
]
)
end
diff --git a/backend/test/richard_burton/publication_test.exs b/backend/test/richard_burton/publication_test.exs
index 9da1eb6b..40d2bc03 100644
--- a/backend/test/richard_burton/publication_test.exs
+++ b/backend/test/richard_burton/publication_test.exs
@@ -12,7 +12,7 @@ defmodule RichardBurton.PublicationTest do
@valid_attrs %{
"title" => "Manuel de Moraes: A Chronicle of the Seventeenth Century",
- "country" => "GB",
+ "countries" => [%{"code" => "GB"}],
"year" => 1886,
"publisher" => "Bickers & Son",
"translated_book" => %{
@@ -34,7 +34,7 @@ defmodule RichardBurton.PublicationTest do
@empty_attrs_error_map %{
title: :required,
- country: :required,
+ countries: :required,
year: :required,
publisher: :required,
translated_book: :required
@@ -42,7 +42,7 @@ defmodule RichardBurton.PublicationTest do
@skeleton_attrs_error_map %{
title: :required,
- country: :required,
+ countries: :required,
year: :required,
publisher: :required,
translated_book: %{
@@ -76,24 +76,24 @@ defmodule RichardBurton.PublicationTest do
refute change_valid(%{"title" => nil}).valid?
end
- test "when country is blank, is invalid" do
- refute change_valid(%{"country" => ""}).valid?
+ test "when countries is blank, is invalid" do
+ refute change_valid(%{"countries" => ""}).valid?
end
- test "when country is nil, is invalid" do
- refute change_valid(%{"country" => nil}).valid?
+ test "when countries is nil, is invalid" do
+ refute change_valid(%{"countries" => nil}).valid?
end
- test "when country is valid alpha3 code, is invalid" do
- refute change_valid(%{"country" => "USA"}).valid?
+ test "when countries is valid alpha3 code, is invalid" do
+ refute change_valid(%{"countries" => "USA"}).valid?
end
- test "when country is invalid 3 digit code, is invalid" do
- refute change_valid(%{"country" => "EUA"}).valid?
+ test "when countries is invalid 3 digit code, is invalid" do
+ refute change_valid(%{"countries" => "EUA"}).valid?
end
- test "when country is invalid 2 digit code, is invalid" do
- refute change_valid(%{"country" => "XX"}).valid?
+ test "when countries is invalid 2 digit code, is invalid" do
+ refute change_valid(%{"countries" => "XX"}).valid?
end
test "when publisher is blank, is invalid" do
diff --git a/backend/test/richard_burton/translated_book_test.exs b/backend/test/richard_burton/translated_book_test.exs
index 2df67dc8..858ead7f 100644
--- a/backend/test/richard_burton/translated_book_test.exs
+++ b/backend/test/richard_burton/translated_book_test.exs
@@ -187,7 +187,7 @@ defmodule RichardBurton.TranslatedBookTest do
describe "link/1" do
@publication_attrs %{
"title" => "Manuel de Moraes: A Chronicle of the Seventeenth Century",
- "country" => "GB",
+ "countries" => [%{"code" => "GB"}],
"year" => 1886,
"publisher" => "Bickers & Son",
"translated_book" => %{
diff --git a/backend/test/richard_burton_web/controllers/publication_controller_test.exs b/backend/test/richard_burton_web/controllers/publication_controller_test.exs
index b718536d..62b6043f 100644
--- a/backend/test/richard_burton_web/controllers/publication_controller_test.exs
+++ b/backend/test/richard_burton_web/controllers/publication_controller_test.exs
@@ -10,7 +10,7 @@ defmodule RichardBurtonWeb.PublicationControllerTest do
@publication_attrs %{
"title" => "Iraçéma the Honey-Lips: A Legend of Brazil",
"year" => "1886",
- "country" => "GB",
+ "countries" => "GB",
"publisher" => "Bickers & Son",
"authors" => "Isabel Burton",
"original_authors" => "José de Alencar",
@@ -35,7 +35,7 @@ defmodule RichardBurtonWeb.PublicationControllerTest do
describe "POST /publications/bulk" do
@valid_input_1 %{
"title" => "Manuel de Moraes: A Chronicle of the Seventeenth Century",
- "country" => "GB",
+ "countries" => "GB",
"year" => 1886,
"publisher" => "Bickers & Son",
"authors" => "Richard Burton, Isabel Burton",
@@ -46,7 +46,7 @@ defmodule RichardBurtonWeb.PublicationControllerTest do
@valid_input_2 %{
"title" => "Iraçéma the Honey-Lips: A Legend of Brazil",
"year" => 1886,
- "country" => "GB",
+ "countries" => "GB",
"publisher" => "Bickers & Son",
"authors" => "Isabel Burton",
"original_authors" => "José de Alencar",
@@ -56,7 +56,7 @@ defmodule RichardBurtonWeb.PublicationControllerTest do
@invalid_input %{
"title" => "",
"year" => "1886",
- "country" => "GB",
+ "countries" => "GB",
"publisher" => "Bickers & Son",
"authors" => "",
"original_authors" => "José de Alencar",
@@ -112,7 +112,7 @@ defmodule RichardBurtonWeb.PublicationControllerTest do
@correct_input_1 %{
"title" => "Iraçéma the Honey-Lips: A Legend of Brazil",
"year" => "1886",
- "country" => "GB",
+ "countries" => "GB",
"publisher" => "Bickers & Son",
"authors" => "Isabel Burton, Richard Burton",
"original_authors" => "José de Alencar",
@@ -121,7 +121,7 @@ defmodule RichardBurtonWeb.PublicationControllerTest do
@correct_input_2 %{
"title" => "Ubirajara: A Legend of the Tupy Indians",
"year" => "1922",
- "country" => "US",
+ "countries" => "US",
"publisher" => "Ronald Massey",
"authors" => "J. T. W. Sadler",
"original_authors" => "José de Alencar",
@@ -130,7 +130,7 @@ defmodule RichardBurtonWeb.PublicationControllerTest do
@correct_input_3 %{
"title" => "",
"year" => "AAAA",
- "country" => "GB",
+ "countries" => "GB",
"publisher" => "Bickers & Son",
"authors" => "",
"original_authors" => "José de Alencar",
@@ -139,7 +139,7 @@ defmodule RichardBurtonWeb.PublicationControllerTest do
@correct_input_4 %{
"title" => "Ubirajara: A Legend of the Tupy Indians",
"year" => "",
- "country" => "",
+ "countries" => "",
"publisher" => "",
"authors" => "J. T. W. Sadler",
"original_authors" => "",
@@ -148,7 +148,7 @@ defmodule RichardBurtonWeb.PublicationControllerTest do
@correct_input_5 %{
"title" => "Ubirajara: A Legend of the Tupy Indians",
"year" => "",
- "country" => "USA",
+ "countries" => "USA",
"publisher" => "",
"authors" => "J. T. W. Sadler",
"original_authors" => "",
@@ -184,7 +184,7 @@ defmodule RichardBurtonWeb.PublicationControllerTest do
"publication" => @correct_input_4,
"errors" => %{
"year" => "required",
- "country" => "required",
+ "countries" => "required",
"publisher" => "required",
"original_authors" => "required",
"original_title" => "required"
@@ -194,7 +194,7 @@ defmodule RichardBurtonWeb.PublicationControllerTest do
"publication" => @correct_input_5,
"errors" => %{
"year" => "required",
- "country" => "alpha2",
+ "countries" => "alpha2",
"publisher" => "required",
"original_authors" => "required",
"original_title" => "required"
@@ -218,7 +218,7 @@ defmodule RichardBurtonWeb.PublicationControllerTest do
"publication" => %{
"title" => "Iraçéma the Honey-Lips: A Legend of Brazil",
"year" => "1886",
- "country" => "GB",
+ "countries" => "GB",
"publisher" => "Bickers & Son",
"authors" => "Isabel Burton, Richard Burton",
"original_authors" => "José de Alencar",
@@ -230,7 +230,7 @@ defmodule RichardBurtonWeb.PublicationControllerTest do
"publication" => %{
"title" => "Ubirajara: A Legend of the Tupy Indians",
"year" => "1922",
- "country" => "US",
+ "countries" => "US, GB",
"publisher" => "Ronald Massey",
"authors" => "J. T. W. Sadler",
"original_authors" => "José de Alencar",
@@ -242,7 +242,7 @@ defmodule RichardBurtonWeb.PublicationControllerTest do
"publication" => %{
"title" => "",
"year" => "AAAA",
- "country" => "GB",
+ "countries" => "GB",
"publisher" => "Bickers & Son",
"authors" => "",
"original_authors" => "José de Alencar",
@@ -258,7 +258,7 @@ defmodule RichardBurtonWeb.PublicationControllerTest do
"publication" => %{
"title" => "Ubirajara: A Legend of the Tupy Indians",
"year" => "",
- "country" => "",
+ "countries" => "",
"publisher" => "",
"authors" => "J. T. W. Sadler",
"original_authors" => "",
@@ -266,7 +266,7 @@ defmodule RichardBurtonWeb.PublicationControllerTest do
},
"errors" => %{
"year" => "required",
- "country" => "required",
+ "countries" => "required",
"publisher" => "required",
"original_authors" => "required",
"original_title" => "required"
@@ -312,7 +312,7 @@ defmodule RichardBurtonWeb.PublicationControllerTest do
conn = get(meta.conn, publication_path(meta.conn, :export))
expected_data =
- "authors;country;original_authors;original_title;publisher;title;year\nIsabel Burton;GB;José de Alencar;Iracema;Bickers & Son;Iraçéma the Honey-Lips: A Legend of Brazil;1886\n"
+ "authors;countries;original_authors;original_title;publisher;title;year\nIsabel Burton;GB;José de Alencar;Iracema;Bickers & Son;Iraçéma the Honey-Lips: A Legend of Brazil;1886\n"
expected_filename = "publications.csv"
expected_content_disposition = ["attachment; filename=\"#{expected_filename}\""]
@@ -342,7 +342,7 @@ defmodule RichardBurtonWeb.PublicationControllerTest do
conn = get(meta.conn, path)
expected_data =
- "authors;country;original_authors;original_title;publisher;title;year\nIsabel Burton;GB;José de Alencar;Iracema;Bickers & Son;Iraçéma the Honey-Lips: A Legend of Brazil;1886\n"
+ "authors;countries;original_authors;original_title;publisher;title;year\nIsabel Burton;GB;José de Alencar;Iracema;Bickers & Son;Iraçéma the Honey-Lips: A Legend of Brazil;1886\n"
expected_filename = "publications-#{search}.csv"
expected_content_disposition = ["attachment; filename=\"#{expected_filename}\""]
diff --git a/frontend/components/DataInput.tsx b/frontend/components/DataInput.tsx
index f6931ae7..e9daced9 100644
--- a/frontend/components/DataInput.tsx
+++ b/frontend/components/DataInput.tsx
@@ -1,27 +1,29 @@
+import {
+ Publication,
+ PublicationId,
+ PublicationKey,
+ PublicationKeyType,
+} from "modules/publication";
import {
FC,
FocusEvent,
- forwardRef,
HTMLProps,
Ref,
+ forwardRef,
useEffect,
useState,
} from "react";
-import TextDataInput from "./TextDataInput";
import TextArrayDataInput from "./TextArrayDataInput";
-import {
- Publication,
- PublicationId,
- PublicationKey,
- PublicationKeyType,
-} from "modules/publication";
-import Tooltip from "./Tooltip";
+import TextDataInput from "./TextDataInput";
+import TextEnumArrayDataInput from "./TextEnumArrayDataInput";
import TextEnumDataInput from "./TextEnumDataInput";
import TextNumberDataInput from "./TextNumberDataInput";
+import Tooltip from "./Tooltip";
const COMPONENTS_PER_TYPE: Record> = {
text: TextDataInput,
enum: TextEnumDataInput,
+ enumArray: TextEnumArrayDataInput,
number: TextNumberDataInput,
array: TextArrayDataInput,
};
diff --git a/frontend/components/Multicombobox.tsx b/frontend/components/Multicombobox.tsx
index eba7c243..e74b45ca 100644
--- a/frontend/components/Multicombobox.tsx
+++ b/frontend/components/Multicombobox.tsx
@@ -1,26 +1,43 @@
-import { forwardRef, HTMLProps, KeyboardEvent, useRef, useState } from "react";
+import { ForwardedRef, KeyboardEvent, useRef, useState } from "react";
import Pill from "./Pill";
-import MenuProvider from "./MenuProvider";
import { Key } from "app";
+import { isString } from "lodash";
+import { z } from "zod";
+import MenuProvider from "./MenuProvider";
import TextInput from "./TextInput";
-type Item = string;
+type Item = { id: string; label: string };
-type Props = Omit, "value" | "onChange"> & {
- value: Item[];
+type Props = {
+ placeholder?: string;
+ value: ItemType[];
error?: string;
- onChange: (value: Item[]) => void;
- getOptions: (search: string) => Promise- | Item[];
+ onKeyDown?: (event: KeyboardEvent) => void;
+ onChange: (value: ItemType[]) => void;
+ getOptions: (search: string) => Promise | ItemType[];
+ forwardedRef?: ForwardedRef;
};
-export default forwardRef(function Multicombobox(
- { value, placeholder, getOptions, onChange, onKeyDown, error, ...props },
- ref,
-) {
+function isStringArray(value: unknown): value is string[] {
+ return z.string().array().safeParse(value).success;
+}
+
+export default function Multicombobox({
+ value,
+ placeholder,
+ getOptions,
+ onChange,
+ onKeyDown,
+ error,
+ forwardedRef,
+ ...props
+}: Props) {
const [inputValue, setInputValue] = useState("");
const [activeIndex, setActiveIndex] = useState(null);
- const [options, setOptions] = useState([]);
+ const [options, setOptions] = useState([]);
+
+ const isEnum = isStringArray(value) && isStringArray(options);
const [isOpen, setIsOpen] = useState(false);
@@ -28,7 +45,7 @@ export default forwardRef(function Multicombobox(
onChange(value.filter((_, i) => i !== index));
}
- function select(item: Item) {
+ function select(item: ItemType) {
if (!value.includes(item)) onChange([...value, item]);
}
@@ -38,10 +55,14 @@ export default forwardRef(function Multicombobox(
if (event.key === Key.COMMA) {
event.preventDefault();
+ if (isEnum) {
+ setOptions([]);
+ }
+
if (!isInputValueBlank) {
setInputValue("");
setOptions([]);
- select(inputValue);
+ select(inputValue as ItemType);
}
}
@@ -56,7 +77,7 @@ export default forwardRef(function Multicombobox(
if (activeIndex != null && options[activeIndex]) {
select(options[activeIndex]);
} else {
- select(inputValue);
+ select(inputValue as ItemType);
}
}
@@ -81,7 +102,7 @@ export default forwardRef(function Multicombobox(
}
}
- function handleOptionSelect(option: string) {
+ function handleOptionSelect(option: ItemType) {
select(option);
setInputValue("");
inputRef.current?.focus();
@@ -90,7 +111,7 @@ export default forwardRef(function Multicombobox(
const inputRef = useRef(null);
return (
-
options={options}
isOpen={isOpen}
activeIndex={activeIndex}
@@ -100,7 +121,7 @@ export default forwardRef(function Multicombobox(
>
(function Multicombobox(
{value.map((item, index) => (
unselect(index)}
/>
))}
@@ -123,4 +144,4 @@ export default forwardRef(function Multicombobox(
/>
);
-});
+}
diff --git a/frontend/components/PublicationIndexList.tsx b/frontend/components/PublicationIndexList.tsx
index 764975ab..12e7876e 100644
--- a/frontend/components/PublicationIndexList.tsx
+++ b/frontend/components/PublicationIndexList.tsx
@@ -28,7 +28,9 @@ const PublicationItem: FC<{ id: number }> = ({ id }) => {
{publication.year}
-
{publication.country}
+
+ {publication.countries}
+
)
diff --git a/frontend/components/PublicationModal.tsx b/frontend/components/PublicationModal.tsx
index 3683d323..aa373b6a 100644
--- a/frontend/components/PublicationModal.tsx
+++ b/frontend/components/PublicationModal.tsx
@@ -1,4 +1,4 @@
-import { COUNTRIES, Publication } from "modules/publication";
+import { Publication } from "modules/publication";
import Link from "next/link";
import { FC } from "react";
import { z } from "zod";
@@ -27,6 +27,21 @@ const Searchable: FC<{ label: string; value?: string }> = ({
);
};
+const SearchableList: FC<{ items: { label: string; value?: string }[] }> = ({
+ items,
+}) => (
+
+ {items.map((item, index) => (
+ -
+ {index === items.length - 1 && " and "}
+
+ {index < items.length - 2 && ", "}
+ {index === items.length - 1 && " "}
+
+ ))}
+
+);
+
const PublicationHeading: FC<{ publication: Publication }> = ({
publication,
}) => (
@@ -52,10 +67,15 @@ const PublicationDescription: FC<{ publication: Publication }> = ({
, by{" "}
. It was written by{" "}
and published in{" "}
- {" "}
+ id.trim())
+ .map((id) => ({
+ id,
+ label: Publication.describeValue(id, "countries"),
+ }))}
+ />
in {publication?.year} by .
);
@@ -67,6 +87,8 @@ const PublicationModal: FC = () => {
const publication = Publication.STORE.usePublication(publicationId);
+ console.log({ publication });
+
return (
{publication && (
diff --git a/frontend/components/TextArrayDataInput.tsx b/frontend/components/TextArrayDataInput.tsx
index 7c3d28e5..4a542669 100644
--- a/frontend/components/TextArrayDataInput.tsx
+++ b/frontend/components/TextArrayDataInput.tsx
@@ -1,11 +1,15 @@
+import { Publication } from "modules/publication";
+import pDebounce from "p-debounce";
import { FC, forwardRef, useCallback, useMemo } from "react";
import { DataInputProps } from "./DataInput";
-import { Publication } from "modules/publication";
import Multicombobox from "./Multicombobox";
-import pDebounce from "p-debounce";
export default forwardRef(
- function TextArrayDataInput({ colId, value, onChange, ...props }, ref) {
+ function TextArrayDataInput(
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ { colId, value, onChange, ...props },
+ ref,
+ ) {
const items = useMemo(
() => (value === "" ? [] : value.split(",")),
[value],
@@ -25,9 +29,9 @@ export default forwardRef(
);
return (
-
{...props}
- ref={ref}
+ forwardedRef={ref}
value={items}
onChange={handleChange}
getOptions={getOptions}
diff --git a/frontend/components/TextEnumArrayDataInput.tsx b/frontend/components/TextEnumArrayDataInput.tsx
new file mode 100644
index 00000000..929f7e4c
--- /dev/null
+++ b/frontend/components/TextEnumArrayDataInput.tsx
@@ -0,0 +1,52 @@
+import { Publication } from "modules/publication";
+import pDebounce from "p-debounce";
+import { FC, forwardRef, useCallback, useMemo } from "react";
+import { DataInputProps } from "./DataInput";
+import Multicombobox from "./Multicombobox";
+
+type Enum = { id: string; label: string };
+
+export default forwardRef(
+ function TextEnumArrayDataInput(
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ { colId, value, onChange, ...props },
+ ref,
+ ) {
+ const toEnum = useCallback(
+ (id: string): Enum => {
+ return { id, label: Publication.describeValue(id, colId) };
+ },
+ [colId],
+ );
+
+ const items = useMemo(
+ () => (value === "" ? [] : value.split(",").map(toEnum)),
+ [value, toEnum],
+ );
+
+ console.log({ value, items });
+
+ function handleChange(value: Enum[]) {
+ onChange?.(value.map(({ id }) => id).join(","));
+ }
+
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ const getOptions = useCallback(
+ pDebounce(
+ (search: string) => Publication.autocomplete(search, colId),
+ 350,
+ ),
+ [colId],
+ );
+
+ return (
+
+ {...props}
+ forwardedRef={ref}
+ value={items}
+ onChange={handleChange}
+ getOptions={getOptions}
+ />
+ );
+ },
+) as FC;
diff --git a/frontend/modules/publication.ts b/frontend/modules/publication.ts
index 46b34259..62b4fb55 100644
--- a/frontend/modules/publication.ts
+++ b/frontend/modules/publication.ts
@@ -24,7 +24,7 @@ import { COUNTRIES, Country } from "./country";
type Publication = {
title: string;
- country: string;
+ countries: string;
year: string;
publisher: string;
authors: string;
@@ -181,7 +181,7 @@ const TOTAL_PUBLICATION_COUNT = selector({
const DEFAULT_ATTRIBUTE_VISIBILITY: Record = {
title: true,
- country: true,
+ countries: true,
year: true,
publisher: true,
authors: true,
@@ -273,7 +273,7 @@ const ERROR_MESSAGES: Record = {
alpha2: "This field should be a valid ISO 3166-1 alpha 2 country code",
};
-type PublicationKeyType = "array" | "text" | "enum" | "number";
+type PublicationKeyType = "array" | "text" | "enum" | "enumArray" | "number";
interface PublicationModule {
ATTRIBUTES: PublicationKey[];
@@ -364,7 +364,7 @@ interface PublicationModule {
useValidate(): (ids: PublicationId[]) => Promise;
};
- autocomplete(value: string, attribute: "country"): Promise;
+ autocomplete(value: string, attribute: "countries"): Promise;
autocomplete(value: string, attribute: "originalAuthors"): Promise;
autocomplete(value: string, attribute: "authors"): Promise;
autocomplete(value: string, attribute: string): Promise<[]>;
@@ -383,14 +383,14 @@ const Publication: PublicationModule = {
"title",
"authors",
"year",
- "country",
+ "countries",
"publisher",
],
ATTRIBUTE_LABELS: {
authors: "Translators",
originalAuthors: "Original Authors",
originalTitle: "Original Title",
- country: "Country",
+ countries: "Country",
publisher: "Publisher",
title: "Title",
year: "Year",
@@ -400,7 +400,7 @@ const Publication: PublicationModule = {
authors: "array",
originalAuthors: "array",
originalTitle: "text",
- country: "enum",
+ countries: "enumArray",
publisher: "text",
title: "text",
year: "number",
@@ -410,7 +410,7 @@ const Publication: PublicationModule = {
authors: true,
originalAuthors: true,
originalTitle: false,
- country: true,
+ countries: true,
publisher: true,
title: false,
year: true,
@@ -833,13 +833,14 @@ const Publication: PublicationModule = {
},
},
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
autocomplete(value, attribute): Promise {
switch (attribute) {
case "authors":
case "originalAuthors":
return Author.REMOTE.search(value);
- case "country": {
+ case "countries": {
const all = Object.values(COUNTRIES);
const countries = value
@@ -863,10 +864,13 @@ const Publication: PublicationModule = {
},
describeValue(value, attribute) {
- if (attribute === "country") {
- const country = COUNTRIES[value];
- if (country) {
- return COUNTRIES[value].label;
+ if (attribute === "countries") {
+ const countries = COUNTRIES[value];
+ if (countries) {
+ return value
+ .split(",")
+ .map((v) => COUNTRIES[v.trim()].label)
+ .join(", ");
} else {
console.warn("Unknown country code: ", value);
return value;
@@ -896,7 +900,7 @@ const Publication: PublicationModule = {
empty() {
return {
authors: "",
- country: "",
+ countries: "",
originalAuthors: "",
originalTitle: "",
publisher: "",
From 8ee41f0327c6b07ebabc2b73a59d12133b0dd65b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9s=20Vidal?=
Date: Fri, 21 Jun 2024 12:58:35 -0300
Subject: [PATCH 2/8] Refactor Author tests to match new Country ones
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Signed-off-by: Andrés Vidal
---
backend/test/richard_burton/author_test.exs | 152 +++++++-------------
1 file changed, 52 insertions(+), 100 deletions(-)
diff --git a/backend/test/richard_burton/author_test.exs b/backend/test/richard_burton/author_test.exs
index 3a79add7..6a25c298 100644
--- a/backend/test/richard_burton/author_test.exs
+++ b/backend/test/richard_burton/author_test.exs
@@ -8,8 +8,6 @@ defmodule RichardBurton.AuthorTest do
alias RichardBurton.Author
alias RichardBurton.Validation
- alias RichardBurton.OriginalBook
- alias RichardBurton.TranslatedBook
@valid_attrs %{"name" => "J. M. Pereira da Silva"}
@@ -41,13 +39,13 @@ defmodule RichardBurton.AuthorTest do
attrs |> changeset() |> Repo.insert!()
end
- defp linked(changeset = %Ecto.Changeset{}) do
+ defp get_authors(changeset = %Ecto.Changeset{}) do
changeset
|> get_change(:authors)
|> Enum.map(&apply_changes/1)
end
- defp linked_fingerprint(changeset = %Ecto.Changeset{}) do
+ defp get_fingerprint(changeset = %Ecto.Changeset{}) do
get_change(changeset, :authors_fingerprint)
end
@@ -114,140 +112,94 @@ defmodule RichardBurton.AuthorTest do
end
end
- describe "link/1" do
- @original_book_attrs %{
- "title" => "Title",
- "authors" => [
- %{"name" => "Richard Burton"},
- %{"name" => "Isabel Burton"}
- ]
- }
-
- @translated_book_attrs %{
- "authors" => [
- %{"name" => "Richard Burton"},
- %{"name" => "Isabel Burton"}
- ],
- "original_book" => %{
- "title" => "Dom Casmurro",
- "authors" => [%{"name" => "Machado de Assis"}]
- }
- }
-
- test "links existing authors to OriginalBook changeset" do
- authors = Enum.map(@original_book_attrs["authors"], &insert!/1)
-
- changeset =
- %OriginalBook{}
- |> OriginalBook.changeset(@original_book_attrs)
- |> Author.link()
+ defmodule WithManyAuthors do
+ use Ecto.Schema
+ import Ecto.Changeset
- assert changeset.valid?
-
- assert authors == linked(changeset)
+ schema "with_many_authors" do
+ field(:authors_fingerprint, :string)
+ has_many :authors, Author
end
- test "links existing authors to TranslatedBook changeset" do
- authors = Enum.map(@translated_book_attrs["authors"], &insert!/1)
-
- changeset =
- %TranslatedBook{}
- |> TranslatedBook.changeset(@translated_book_attrs)
- |> Author.link()
-
- assert changeset.valid?
-
- assert authors == linked(changeset)
+ def changeset(attrs) do
+ %WithManyAuthors{} |> cast(attrs, []) |> cast_assoc(:authors)
end
+ end
- test "links non-existing authors to OriginalBook changeset, inserting them" do
- changeset =
- %OriginalBook{}
- |> OriginalBook.changeset(@original_book_attrs)
- |> Author.link()
-
- assert changeset.valid?
+ describe "link/1" do
+ test "links existing authors to changeset" do
+ attrs = %{
+ "authors" => [
+ %{"name" => "Richard Burton"},
+ %{"name" => "Isabel Burton"}
+ ]
+ }
- assert Author.all() == linked(changeset)
- end
+ authors = Enum.map(attrs["authors"], &insert!/1)
- test "links non-existing authors to TranslatedBook changeset, inserting them" do
changeset =
- %TranslatedBook{}
- |> TranslatedBook.changeset(@translated_book_attrs)
+ attrs
+ |> WithManyAuthors.changeset()
|> Author.link()
assert changeset.valid?
-
- assert Author.all() == linked(changeset)
+ assert authors == get_authors(changeset)
end
- test "has no side effects when OriginalBook changeset is invalid" do
+ test "links non-existing authors to changeset, inserting them" do
+ attrs = %{
+ "authors" => [
+ %{"name" => "Richard Burton"},
+ %{"name" => "Isabel Burton"}
+ ]
+ }
+
changeset =
- %OriginalBook{}
- |> OriginalBook.changeset(%{})
+ attrs
+ |> WithManyAuthors.changeset()
|> Author.link()
- refute changeset.valid?
-
- assert Enum.empty?(Author.all())
+ assert changeset.valid?
+ assert Author.all() == get_authors(changeset)
end
- test "has no side effects when TranslatedBook changeset is invalid" do
+ test "has no side effects when changeset is invalid" do
changeset =
- %TranslatedBook{}
- |> TranslatedBook.changeset(%{})
+ %{"authors" => [%{}]}
+ |> WithManyAuthors.changeset()
|> Author.link()
refute changeset.valid?
-
assert Enum.empty?(Author.all())
end
end
describe "link_fingerprint/1" do
- test "links fingerprint using author names to OriginalBook changeset" do
- changeset =
- %OriginalBook{}
- |> OriginalBook.changeset(@original_book_attrs)
- |> Author.link_fingerprint()
-
- assert changeset.valid?
-
- assert Author.fingerprint(linked(changeset)) == linked_fingerprint(changeset)
- end
+ test "links fingerprint using author names to changeset" do
+ attrs = %{
+ "authors" => [
+ %{"name" => "Richard Burton"},
+ %{"name" => "Isabel Burton"}
+ ]
+ }
- test "links fingerprint using author names to TranslatedBook changeset" do
changeset =
- %TranslatedBook{}
- |> TranslatedBook.changeset(@translated_book_attrs)
+ attrs
+ |> WithManyAuthors.changeset()
|> Author.link_fingerprint()
assert changeset.valid?
-
- assert Author.fingerprint(linked(changeset)) == linked_fingerprint(changeset)
+ assert Author.fingerprint(get_authors(changeset)) == get_fingerprint(changeset)
end
- test "does not link fingerprint to invalid OriginalBook changeset" do
+ test "does not link fingerprint to invalid changeset" do
changeset =
- %OriginalBook{}
- |> OriginalBook.changeset(%{})
+ %{"authors" => [%{}]}
+ |> WithManyAuthors.changeset()
|> Author.link_fingerprint()
refute changeset.valid?
-
- assert is_nil(linked_fingerprint(changeset))
- end
-
- test "does not link fingerprint to invalid TranslatedBook changeset" do
- changeset =
- %TranslatedBook{}
- |> TranslatedBook.changeset(%{})
- |> Author.link_fingerprint()
-
- refute changeset.valid?
-
- assert is_nil(linked_fingerprint(changeset))
+ assert is_nil(get_fingerprint(changeset))
end
end
From c9cad8bdb427e3baafadc4c91203e4ce3de3105d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9s=20Vidal?=
Date: Fri, 21 Jun 2024 13:00:36 -0300
Subject: [PATCH 3/8] Move functions away from Publication.Codec
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Signed-off-by: Andrés Vidal
---
backend/lib/richard_burton/author.ex | 12 ++++++
backend/lib/richard_burton/country.ex | 19 +++++++--
.../lib/richard_burton/publication_codec.ex | 39 +++++--------------
backend/test/richard_burton/author_test.exs | 37 ++++++++++++++++++
backend/test/richard_burton/country_test.exs | 37 ++++++++++++++++++
5 files changed, 110 insertions(+), 34 deletions(-)
diff --git a/backend/lib/richard_burton/author.ex b/backend/lib/richard_burton/author.ex
index d8d69a7e..e0365c99 100644
--- a/backend/lib/richard_burton/author.ex
+++ b/backend/lib/richard_burton/author.ex
@@ -81,6 +81,7 @@ defmodule RichardBurton.Author do
def link_fingerprint(changeset = %Ecto.Changeset{valid?: false}), do: changeset
+ @spec search(binary(), :fuzzy | :prefix) :: any()
def search(term, :prefix) when is_binary(term) do
from(a in Author, where: ilike(a.name, ^"#{term}%"))
|> Repo.all()
@@ -97,4 +98,15 @@ defmodule RichardBurton.Author do
keywords when is_list(keywords) -> keywords
end
end
+
+ def nest(authors) when is_binary(authors) do
+ authors |> String.split(",") |> Enum.map(&%{"name" => String.trim(&1)})
+ end
+
+ def flatten(authors) when is_list(authors), do: Enum.map_join(authors, ", ", &get_name/1)
+ def flatten(authors), do: authors
+
+ def get_name(%Author{name: name}), do: name
+ def get_name(%{"name" => name}), do: name
+ def get_name(%{name: name}), do: name
end
diff --git a/backend/lib/richard_burton/country.ex b/backend/lib/richard_burton/country.ex
index 0a2ac1b2..fc4868d9 100644
--- a/backend/lib/richard_burton/country.ex
+++ b/backend/lib/richard_burton/country.ex
@@ -59,9 +59,9 @@ defmodule RichardBurton.Country do
def validate_countries(countries) when is_binary(countries) do
invalid =
- String.split(countries, ",")
- |> Enum.map(&String.trim/1)
- |> Enum.map(fn code -> changeset(%Country{}, %{"code" => code}) end)
+ countries
+ |> nest()
+ |> Enum.map(&changeset(%Country{}, &1))
|> Enum.reject(fn cset -> cset.valid? end)
message = "Invalid countries: #{Enum.map_join(invalid, ", ", &get_change(&1, :code))}"
@@ -75,7 +75,7 @@ defmodule RichardBurton.Country do
@spec fingerprint(binary() | maybe_improper_list()) :: binary()
def fingerprint(countries) when is_binary(countries) do
countries
- |> Publication.Codec.nest_countries()
+ |> nest()
|> Enum.map(fn %{"code" => code} -> %Country{code: code} end)
|> fingerprint()
end
@@ -120,4 +120,15 @@ defmodule RichardBurton.Country do
end
def link_fingerprint(changeset = %Ecto.Changeset{valid?: false}), do: changeset
+
+ def nest(countries) when is_binary(countries) do
+ countries |> String.split(",") |> Enum.map(&%{"code" => String.trim(&1)})
+ end
+
+ def flatten(countries) when is_list(countries), do: Enum.map_join(countries, ", ", &get_code/1)
+ def flatten(countries), do: countries
+
+ def get_code(%Country{code: code}), do: code
+ def get_code(%{"code" => code}), do: code
+ def get_code(%{code: code}), do: code
end
diff --git a/backend/lib/richard_burton/publication_codec.ex b/backend/lib/richard_burton/publication_codec.ex
index a4a50d5c..c9fc3cda 100644
--- a/backend/lib/richard_burton/publication_codec.ex
+++ b/backend/lib/richard_burton/publication_codec.ex
@@ -3,7 +3,9 @@ defmodule RichardBurton.Publication.Codec do
Serialization and deserialization utilities for publications
"""
+ alias RichardBurton.Author
alias RichardBurton.Codec
+ alias RichardBurton.Country
alias RichardBurton.Util
alias RichardBurton.Publication
alias RichardBurton.FlatPublication
@@ -87,13 +89,13 @@ defmodule RichardBurton.Publication.Codec do
end
defp nest_entry({"authors", value}),
- do: {"authors", nest_authors(value)}
+ do: {"authors", Author.nest(value)}
defp nest_entry({"original_authors", value}),
- do: {"original_authors", nest_authors(value)}
+ do: {"original_authors", Author.nest(value)}
defp nest_entry({"countries", value}),
- do: {"countries", nest_countries(value)}
+ do: {"countries", Country.nest(value)}
defp nest_entry({key, value}),
do: {key, value}
@@ -102,10 +104,6 @@ defmodule RichardBurton.Publication.Codec do
Enum.map(String.split(authors, ","), &%{"name" => String.trim(&1)})
end
- def nest_countries(countries) when is_binary(countries) do
- Enum.map(String.split(countries, ","), &%{"code" => String.trim(&1)})
- end
-
def flatten(publication = %Publication{}) do
attrs =
publication
@@ -135,29 +133,10 @@ defmodule RichardBurton.Publication.Codec do
publication_like_map |> Codec.flatten() |> Map.new(&(&1 |> rename_key |> flatten_entry))
end
- defp flatten_entry({"authors", value}),
- do: {"authors", flatten_authors(value)}
-
- defp flatten_entry({"original_authors", value}),
- do: {"original_authors", flatten_authors(value)}
-
- defp flatten_entry({"countries", value}),
- do: {"countries", flatten_countries(value)}
-
- defp flatten_entry({key, value}),
- do: {key, value}
-
- defp flatten_authors(authors) when is_list(authors) do
- Enum.map_join(authors, ", ", &(Map.get(&1, "name") || Map.get(&1, :name)))
- end
-
- defp flatten_authors(authors), do: authors
-
- defp flatten_countries(countries) when is_list(countries) do
- Enum.map_join(countries, ", ", &(Map.get(&1, "code") || Map.get(&1, :code)))
- end
-
- defp flatten_countries(countries), do: countries
+ defp flatten_entry({"authors", value}), do: {"authors", Author.flatten(value)}
+ defp flatten_entry({"original_authors", value}), do: {"original_authors", Author.flatten(value)}
+ defp flatten_entry({"countries", value}), do: {"countries", Country.flatten(value)}
+ defp flatten_entry({key, value}), do: {key, value}
defp rename_key({"translated_book_authors", v}), do: {"authors", v}
defp rename_key({"translated_book_original_book_title", v}), do: {"original_title", v}
diff --git a/backend/test/richard_burton/author_test.exs b/backend/test/richard_burton/author_test.exs
index 6a25c298..531e2aca 100644
--- a/backend/test/richard_burton/author_test.exs
+++ b/backend/test/richard_burton/author_test.exs
@@ -268,4 +268,41 @@ defmodule RichardBurton.AuthorTest do
end
end
end
+
+ describe "flatten/1" do
+ test "with a list of maps with string keys, returns a list of maps with code key" do
+ authors = [
+ %{"name" => "Richard Burton"},
+ %{"name" => "Isabel Burton"}
+ ]
+
+ assert "Richard Burton, Isabel Burton" = Author.flatten(authors)
+ end
+
+ test "with a list of maps with atom keys, returns a list of maps with code key" do
+ authors = [
+ %{name: "Richard Burton"},
+ %{name: "Isabel Burton"}
+ ]
+
+ assert "Richard Burton, Isabel Burton" = Author.flatten(authors)
+ end
+
+ test "with a list of Author structs, returns a list of maps with code key" do
+ authors = [
+ %Author{name: "Richard Burton"},
+ %Author{name: "Isabel Burton"}
+ ]
+
+ assert "Richard Burton, Isabel Burton" = Author.flatten(authors)
+ end
+ end
+
+ describe "nest/1" do
+ test "with a comma separated string, returns a list of maps with code key" do
+ authors = "Richard Burton, Isabel Burton"
+
+ assert [%{"name" => "Richard Burton"}, %{"name" => "Isabel Burton"}] = Author.nest(authors)
+ end
+ end
end
diff --git a/backend/test/richard_burton/country_test.exs b/backend/test/richard_burton/country_test.exs
index a9df5e04..872c29fa 100644
--- a/backend/test/richard_burton/country_test.exs
+++ b/backend/test/richard_burton/country_test.exs
@@ -309,4 +309,41 @@ defmodule RichardBurton.CountryTest do
assert is_nil(get_fingerprint(changeset))
end
end
+
+ describe "flatten/1" do
+ test "with a list of maps with string keys, returns a list of maps with code key" do
+ countries = [
+ %{"code" => "GB"},
+ %{"code" => "US"}
+ ]
+
+ assert "GB, US" = Country.flatten(countries)
+ end
+
+ test "with a list of maps with atom keys, returns a list of maps with code key" do
+ countries = [
+ %{code: "GB"},
+ %{code: "US"}
+ ]
+
+ assert "GB, US" = Country.flatten(countries)
+ end
+
+ test "with a list of Country structs, returns a list of maps with code key" do
+ countries = [
+ %Country{code: "GB"},
+ %Country{code: "US"}
+ ]
+
+ assert "GB, US" = Country.flatten(countries)
+ end
+ end
+
+ describe "nest/1" do
+ test "with a comma separated string, returns a list of maps with code key" do
+ countries = "GB, US"
+
+ assert [%{"code" => "GB"}, %{"code" => "US"}] = Country.nest(countries)
+ end
+ end
end
From be2b0948ea0adacf0d0f41c906c90bb34f7f7459 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9s=20Vidal?=
Date: Sun, 23 Jun 2024 19:22:30 -0300
Subject: [PATCH 4/8] Allow a single publication to have multiple publishers
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Signed-off-by: Andrés Vidal
---
.../lib/richard_burton/flat_publication.ex | 9 +-
backend/lib/richard_burton/publication.ex | 29 +-
.../lib/richard_burton/publication_codec.ex | 9 +-
backend/lib/richard_burton/publisher.ex | 99 ++++
.../20240623200257_create_publishers.exs | 459 ++++++++++++++++++
.../richard_burton/flat_publication_test.exs | 12 +-
.../richard_burton/publication_codec_test.exs | 67 +--
.../richard_burton/publication_index_test.exs | 53 +-
.../test/richard_burton/publication_test.exs | 14 +-
.../test/richard_burton/publisher_test.exs | 236 +++++++++
.../richard_burton/translated_book_test.exs | 2 +-
.../publication_controller_test.exs | 36 +-
frontend/components/PublicationIndexList.tsx | 2 +-
frontend/components/PublicationModal.tsx | 14 +-
frontend/modules/publication.ts | 14 +-
15 files changed, 950 insertions(+), 105 deletions(-)
create mode 100644 backend/lib/richard_burton/publisher.ex
create mode 100644 backend/priv/repo/migrations/20240623200257_create_publishers.exs
create mode 100644 backend/test/richard_burton/publisher_test.exs
diff --git a/backend/lib/richard_burton/flat_publication.ex b/backend/lib/richard_burton/flat_publication.ex
index b635fb1a..07cd2041 100644
--- a/backend/lib/richard_burton/flat_publication.ex
+++ b/backend/lib/richard_burton/flat_publication.ex
@@ -7,6 +7,7 @@ defmodule RichardBurton.FlatPublication do
import Ecto.Query
alias RichardBurton.FlatPublication
+ alias RichardBurton.Publisher
alias RichardBurton.Repo
alias RichardBurton.TranslatedBook
alias RichardBurton.Validation
@@ -16,7 +17,7 @@ defmodule RichardBurton.FlatPublication do
:title,
:year,
:countries,
- :publisher,
+ :publishers,
:authors,
:original_title,
:original_authors
@@ -28,12 +29,13 @@ defmodule RichardBurton.FlatPublication do
field(:year, :integer)
field(:countries, :string)
field(:authors, :string)
- field(:publisher, :string)
+ field(:publishers, :string)
field(:original_title, :string)
field(:original_authors, :string)
field(:countries_fingerprint, :string)
field(:translated_book_fingerprint, :string)
+ field(:publishers_fingerprint, :string)
end
@doc false
@@ -43,6 +45,7 @@ defmodule RichardBurton.FlatPublication do
|> validate_required(@external_attributes)
|> Country.validate_countries()
|> Country.link_fingerprint()
+ |> Publisher.link_fingerprint()
|> TranslatedBook.link_fingerprint()
end
@@ -61,7 +64,7 @@ defmodule RichardBurton.FlatPublication do
:title,
:year,
:countries_fingerprint,
- :publisher,
+ :publishers_fingerprint,
:translated_book_fingerprint
],
&{&1, get_field(changeset, &1)}
diff --git a/backend/lib/richard_burton/publication.ex b/backend/lib/richard_burton/publication.ex
index 6ad0844c..f688cef3 100644
--- a/backend/lib/richard_burton/publication.ex
+++ b/backend/lib/richard_burton/publication.ex
@@ -7,25 +7,27 @@ defmodule RichardBurton.Publication do
require Ecto.Query
+ alias RichardBurton.Country
alias RichardBurton.Publication
+ alias RichardBurton.Publisher
alias RichardBurton.Repo
alias RichardBurton.TranslatedBook
alias RichardBurton.Validation
- alias RichardBurton.Country
- @external_attributes [:countries, :publisher, :title, :year, :translated_book]
+ @external_attributes [:countries, :publishers, :title, :year, :translated_book]
@derive {Jason.Encoder, only: @external_attributes}
schema "publications" do
- field(:publisher, :string)
field(:title, :string)
field(:year, :integer)
field(:translated_book_fingerprint, :string)
field(:countries_fingerprint, :string)
+ field(:publishers_fingerprint, :string)
belongs_to(:translated_book, TranslatedBook)
many_to_many(:countries, Country, join_through: "publication_countries")
+ many_to_many(:publishers, Publisher, join_through: "publication_publishers")
timestamps()
end
@@ -41,13 +43,20 @@ defmodule RichardBurton.Publication do
@doc false
def changeset(publication, attrs) do
publication
- |> cast(attrs, [:title, :year, :publisher])
+ |> cast(attrs, [:title, :year])
|> cast_assoc(:translated_book, required: true)
|> cast_assoc(:countries, required: true)
+ |> cast_assoc(:publishers, required: true)
|> validate_length(:countries, min: 1)
- |> validate_required([:title, :year, :publisher])
+ |> validate_required([:title, :year])
|> unique_constraint(
- [:title, :year, :publisher, :countries_fingerprint, :translated_book_fingerprint],
+ [
+ :title,
+ :year,
+ :publishers_fingerprint,
+ :countries_fingerprint,
+ :translated_book_fingerprint
+ ],
name: "publications_composite_key"
)
|> link_fingerprints()
@@ -60,7 +69,11 @@ defmodule RichardBurton.Publication do
end
def preload(data) do
- Repo.preload(data, [:countries, translated_book: [:authors, original_book: [:authors]]])
+ Repo.preload(data, [
+ :countries,
+ :publishers,
+ translated_book: [:authors, original_book: [:authors]]
+ ])
end
def insert(attrs) do
@@ -85,12 +98,14 @@ defmodule RichardBurton.Publication do
changeset
|> TranslatedBook.link_fingerprint()
|> Country.link_fingerprint()
+ |> Publisher.link_fingerprint()
end
defp link_assocs(changeset) do
changeset
|> Country.link()
|> TranslatedBook.link()
+ |> Publisher.link()
end
def insert_all(attrs_list) do
diff --git a/backend/lib/richard_burton/publication_codec.ex b/backend/lib/richard_burton/publication_codec.ex
index c9fc3cda..a92bdd4e 100644
--- a/backend/lib/richard_burton/publication_codec.ex
+++ b/backend/lib/richard_burton/publication_codec.ex
@@ -8,13 +8,14 @@ defmodule RichardBurton.Publication.Codec do
alias RichardBurton.Country
alias RichardBurton.Util
alias RichardBurton.Publication
+ alias RichardBurton.Publisher
alias RichardBurton.FlatPublication
@empty_flat_attrs %{
"title" => "",
"year" => "",
"countries" => "",
- "publisher" => "",
+ "publishers" => "",
"authors" => "",
"original_title" => "",
"original_authors" => ""
@@ -27,7 +28,7 @@ defmodule RichardBurton.Publication.Codec do
"original_title",
"title",
"authors",
- "publisher"
+ "publishers"
]
def from_csv(path) do
@@ -97,6 +98,9 @@ defmodule RichardBurton.Publication.Codec do
defp nest_entry({"countries", value}),
do: {"countries", Country.nest(value)}
+ defp nest_entry({"publishers", value}),
+ do: {"publishers", Publisher.nest(value)}
+
defp nest_entry({key, value}),
do: {key, value}
@@ -136,6 +140,7 @@ defmodule RichardBurton.Publication.Codec do
defp flatten_entry({"authors", value}), do: {"authors", Author.flatten(value)}
defp flatten_entry({"original_authors", value}), do: {"original_authors", Author.flatten(value)}
defp flatten_entry({"countries", value}), do: {"countries", Country.flatten(value)}
+ defp flatten_entry({"publishers", value}), do: {"publishers", Publisher.flatten(value)}
defp flatten_entry({key, value}), do: {key, value}
defp rename_key({"translated_book_authors", v}), do: {"authors", v}
diff --git a/backend/lib/richard_burton/publisher.ex b/backend/lib/richard_burton/publisher.ex
new file mode 100644
index 00000000..966ee4e0
--- /dev/null
+++ b/backend/lib/richard_burton/publisher.ex
@@ -0,0 +1,99 @@
+defmodule RichardBurton.Publisher do
+ @moduledoc """
+ Schema for publishers
+ """
+ use Ecto.Schema
+ import Ecto.Changeset
+
+ alias RichardBurton.Publisher
+ alias RichardBurton.Repo
+ alias RichardBurton.Publication
+ alias RichardBurton.Util
+
+ @derive {Jason.Encoder, only: [:name]}
+ schema "publishers" do
+ field(:name, :string)
+
+ many_to_many(:publications, Publication, join_through: "publication_publishers")
+
+ timestamps()
+ end
+
+ @doc false
+ def changeset(publisher, attrs \\ %{})
+
+ @doc false
+ def changeset(publisher, attrs = %Publisher{}) do
+ changeset(publisher, Map.from_struct(attrs))
+ end
+
+ @doc false
+ def changeset(publisher, attrs) do
+ publisher
+ |> cast(attrs, [:name])
+ |> validate_required([:name])
+ |> unique_constraint(:name)
+ end
+
+ @spec fingerprint(binary() | maybe_improper_list()) :: binary()
+ def fingerprint(publishers) when is_binary(publishers) do
+ publishers
+ |> nest()
+ |> Enum.map(fn %{"name" => name} -> %Publisher{name: name} end)
+ |> fingerprint()
+ end
+
+ def fingerprint(publishers) when is_list(publishers) do
+ publishers
+ |> Enum.map(fn %Publisher{name: name} -> name end)
+ |> Enum.sort()
+ |> Enum.join()
+ |> Util.create_fingerprint()
+ end
+
+ def maybe_insert!(attrs) do
+ %__MODULE__{}
+ |> changeset(attrs)
+ |> Repo.maybe_insert!([:name])
+ end
+
+ def all do
+ Repo.all(Publisher)
+ end
+
+ def link(changeset = %{valid?: true}) do
+ publishers =
+ changeset
+ |> get_change(:publishers)
+ |> Enum.map(&apply_changes/1)
+ |> Enum.map(&maybe_insert!/1)
+
+ put_assoc(changeset, :publishers, publishers)
+ end
+
+ def link(changeset = %{valid?: false}), do: changeset
+
+ def link_fingerprint(changeset = %Ecto.Changeset{valid?: true}) do
+ publishers_fingerprint =
+ changeset
+ |> get_field(:publishers)
+ |> fingerprint
+
+ put_change(changeset, :publishers_fingerprint, publishers_fingerprint)
+ end
+
+ def link_fingerprint(changeset = %Ecto.Changeset{valid?: false}), do: changeset
+
+ def nest(publishers) when is_binary(publishers) do
+ publishers |> String.split(",") |> Enum.map(&%{"name" => String.trim(&1)})
+ end
+
+ def flatten(publishers) when is_list(publishers),
+ do: Enum.map_join(publishers, ", ", &get_name/1)
+
+ def flatten(publishers), do: publishers
+
+ def get_name(%Publisher{name: name}), do: name
+ def get_name(%{"name" => name}), do: name
+ def get_name(%{name: name}), do: name
+end
diff --git a/backend/priv/repo/migrations/20240623200257_create_publishers.exs b/backend/priv/repo/migrations/20240623200257_create_publishers.exs
new file mode 100644
index 00000000..e12ebc88
--- /dev/null
+++ b/backend/priv/repo/migrations/20240623200257_create_publishers.exs
@@ -0,0 +1,459 @@
+defmodule RichardBurton.Repo.Migrations.CreatePublishers do
+ use Ecto.Migration
+
+ def change do
+ create table(:publishers) do
+ add :name, :string
+ timestamps()
+ end
+
+ create unique_index(:publishers, :name)
+
+ execute "-- Up
+
+ INSERT INTO publishers(
+ name,
+ inserted_at,
+ updated_at
+ )
+ SELECT DISTINCT
+ btrim(unnest(string_to_array(publisher, ','))) AS publisher,
+ now(),
+ now()
+ FROM
+ publications;
+ ",
+ "-- Down
+
+ DELETE FROM publishers;
+ "
+
+ create table(:publication_publishers) do
+ add :publication_id, references(:publications)
+ add :publisher_id, references(:publishers)
+ end
+
+ create unique_index(:publication_publishers, [:publication_id, :publisher_id])
+
+ execute "-- Up
+
+ INSERT INTO publication_publishers (
+ publication_id,
+ publisher_id
+ )
+ SELECT
+ publications.id AS publication_id,
+ publishers.id AS publisher_id
+ FROM
+ publications
+ INNER JOIN
+ publishers
+ ON
+ publishers.name = ANY(
+ SELECT
+ btrim(unnest(string_to_array(publications.publisher, ',')))
+ FROM
+ publications AS aux
+ WHERE
+ publications.id = aux.id
+ );
+
+ ",
+ "-- Down
+
+ DELETE FROM publication_publishers;
+ "
+
+ alter table("publications") do
+ add :publishers_fingerprint, :string
+ end
+
+ execute(
+ "--Up
+ UPDATE publications
+ SET publishers_fingerprint = (
+ SELECT
+ encode(sha256(decode(string_agg(publishers.name, '' ORDER BY publishers.name), 'escape')), 'hex') AS fingerprint
+ FROM
+ publication_publishers
+ INNER JOIN
+ publishers ON publication_publishers.publisher_id = publishers.id
+ WHERE
+ publication_publishers.publication_id = publications.id
+ GROUP BY
+ publication_publishers.publication_id
+ )
+ ",
+ "--Down
+ "
+ )
+
+ drop(
+ unique_index(
+ :publications,
+ [
+ :title,
+ :year,
+ :publisher,
+ :translated_book_fingerprint,
+ :countries_fingerprint
+ ],
+ name: "publications_composite_key"
+ )
+ )
+
+ execute "-- Up
+ ALTER TABLE publications
+ ALTER COLUMN publishers_fingerprint SET NOT NULL;
+ ",
+ "-- Down
+
+ ALTER TABLE publications
+ ALTER COLUMN publishers_fingerprint DROP NOT NULL;
+ "
+
+ create unique_index(
+ :publications,
+ [
+ :title,
+ :year,
+ :publishers_fingerprint,
+ :translated_book_fingerprint,
+ :countries_fingerprint
+ ],
+ name: "publications_composite_key"
+ )
+
+ execute "--Up
+
+ DROP MATERIALIZED VIEW search_documents;
+ ",
+ "--Down
+
+ CREATE MATERIALIZED VIEW search_documents AS
+ SELECT
+ id,
+ to_tsvector('simple'::regconfig, title) ||
+ to_tsvector('simple'::regconfig, countries) ||
+ to_tsvector('simple'::regconfig, publisher) ||
+ to_tsvector('simple'::regconfig, year::text) ||
+ to_tsvector('simple'::regconfig, authors) ||
+ to_tsvector('simple'::regconfig, original_title) ||
+ to_tsvector('simple'::regconfig, original_authors) AS document
+ FROM
+ flat_publications;
+ "
+
+ execute "--Up
+
+ DROP VIEW flat_publications
+ ",
+ "--Down
+ "
+
+ execute "--Up
+
+ CREATE OR REPLACE VIEW flat_publications AS
+ WITH
+ CTE_publications AS (
+ SELECT
+ publications.id AS id,
+ publications.title AS title,
+ publications.year AS year,
+ authors.name AS original_author,
+ translators.name AS translator,
+ original_books.title AS original_title,
+ publications.translated_book_fingerprint AS translated_book_fingerprint,
+ publications.countries_fingerprint AS countries_fingerprint,
+ publications.publishers_fingerprint AS publishers_fingerprint,
+ countries.code AS country,
+ publishers.name AS publisher
+ FROM
+ translated_books
+ INNER JOIN
+ publications
+ ON publications.translated_book_id = translated_books.id
+ INNER JOIN
+ original_books
+ ON original_books.id = translated_books.original_book_id
+ INNER JOIN
+ original_book_authors
+ ON original_book_authors.original_book_id = original_books.id
+ INNER JOIN
+ authors
+ ON authors.id = original_book_authors.author_id
+ INNER JOIN
+ translated_book_authors
+ ON translated_book_authors.translated_book_id = translated_books.id
+ INNER JOIN
+ authors AS translators
+ ON translators.id = translated_book_authors.author_id
+ INNER JOIN
+ publication_countries
+ ON publication_countries.publication_id = publications.id
+ INNER JOIN
+ countries
+ ON countries.id = publication_countries.country_id
+ INNER JOIN
+ publication_publishers
+ ON publication_publishers.publication_id = publications.id
+ INNER JOIN
+ publishers
+ ON publishers.id = publication_publishers.publisher_id
+ ),
+ CTE_authors AS (
+ SELECT id, original_author
+ FROM CTE_publications
+ GROUP BY id, original_author
+ ),
+ CTE_authors_distinct AS (
+ SELECT id, string_agg(original_author, ', ' ORDER BY original_author) AS original_authors
+ FROM CTE_authors
+ GROUP BY id
+ ),
+ CTE_translators AS (
+ SELECT id, translator
+ FROM CTE_publications
+ GROUP BY id, translator
+ ),
+ CTE_translators_distinct AS (
+ SELECT id, string_agg(translator, ', ' ORDER BY translator) AS translators
+ FROM CTE_translators
+ GROUP BY id
+ ),
+ CTE_countries AS (
+ SELECT id, country
+ FROM CTE_publications
+ GROUP BY id, country
+ ),
+ CTE_countries_distinct AS (
+ SELECT id, string_agg(country, ', ' ORDER BY country) AS countries
+ FROM CTE_countries
+ GROUP BY id
+ ),
+ CTE_publishers AS (
+ SELECT id, publisher
+ FROM CTE_publications
+ GROUP BY id, publisher
+ ),
+ CTE_publishers_distinct AS (
+ SELECT id, string_agg(publisher, ', ' ORDER BY publisher) AS publishers
+ FROM CTE_publishers
+ GROUP BY id
+ )
+ SELECT
+ CTE_publications.id AS id,
+ CTE_publications.title AS title,
+ CTE_countries_distinct.countries AS countries,
+ CTE_publications.countries_fingerprint AS countries_fingerprint,
+ CTE_publications.year AS year,
+ CTE_publishers_distinct.publishers AS publishers,
+ CTE_publications.publishers_fingerprint AS publishers_fingerprint,
+ CTE_publications.original_title AS original_title,
+ CTE_authors_distinct.original_authors AS original_authors,
+ CTE_translators_distinct.translators AS authors,
+ CTE_publications.translated_book_fingerprint AS translated_book_fingerprint
+ FROM
+ CTE_publications
+ INNER JOIN
+ CTE_authors_distinct
+ ON CTE_publications.id = CTE_authors_distinct.id
+ INNER JOIN
+ CTE_translators_distinct
+ ON CTE_publications.id = CTE_translators_distinct.id
+ INNER JOIN
+ CTE_countries_distinct
+ ON CTE_publications.id = CTE_countries_distinct.id
+ INNER JOIN
+ CTE_publishers_distinct
+ ON CTE_publications.id = CTE_publishers_distinct.id
+ GROUP BY
+ CTE_publications.id,
+ CTE_publications.title,
+ CTE_publications.year,
+ CTE_publications.original_title,
+ CTE_publications.translated_book_fingerprint,
+ CTE_publications.countries_fingerprint,
+ CTE_publications.publishers_fingerprint,
+ CTE_authors_distinct.original_authors,
+ CTE_translators_distinct.translators,
+ CTE_countries_distinct.countries,
+ CTE_publishers_distinct.publishers;
+ ",
+ "-- Down
+
+ CREATE OR REPLACE VIEW flat_publications AS
+ WITH
+ CTE_publications AS (
+ SELECT
+ publications.id AS id,
+ publications.title AS title,
+ publications.year AS year,
+ publications.publisher AS publisher,
+ authors.name AS original_author,
+ translators.name AS translator,
+ original_books.title AS original_title,
+ publications.translated_book_fingerprint AS translated_book_fingerprint,
+ publications.countries_fingerprint AS countries_fingerprint,
+ countries.code AS country
+ FROM
+ translated_books
+ INNER JOIN
+ publications
+ ON publications.translated_book_id = translated_books.id
+ INNER JOIN
+ original_books
+ ON original_books.id = translated_books.original_book_id
+ INNER JOIN
+ original_book_authors
+ ON original_book_authors.original_book_id = original_books.id
+ INNER JOIN
+ authors
+ ON authors.id = original_book_authors.author_id
+ INNER JOIN
+ translated_book_authors
+ ON translated_book_authors.translated_book_id = translated_books.id
+ INNER JOIN
+ authors AS translators
+ ON translators.id = translated_book_authors.author_id
+ INNER JOIN
+ publication_countries
+ ON publication_countries.publication_id = publications.id
+ INNER JOIN
+ countries
+ ON countries.id = publication_countries.country_id
+ ),
+ CTE_authors AS (
+ SELECT id, original_author
+ FROM CTE_publications
+ GROUP BY id, original_author
+ ),
+ CTE_authors_distinct AS (
+ SELECT id, string_agg(original_author, ', ' ORDER BY original_author) AS original_authors
+ FROM CTE_authors
+ GROUP BY id
+ ),
+ CTE_translators AS (
+ SELECT id, translator
+ FROM CTE_publications
+ GROUP BY id, translator
+ ),
+ CTE_translators_distinct AS (
+ SELECT id, string_agg(translator, ', ' ORDER BY translator) AS translators
+ FROM CTE_translators
+ GROUP BY id
+ ),
+ CTE_countries AS (
+ SELECT id, country
+ FROM CTE_publications
+ GROUP BY id, country
+ ),
+ CTE_countries_distinct AS (
+ SELECT id, string_agg(country, ', ' ORDER BY country) AS countries
+ FROM CTE_countries
+ GROUP BY id
+ )
+ SELECT
+ CTE_publications.id AS id,
+ CTE_publications.title AS title,
+ CTE_countries_distinct.countries AS countries,
+ CTE_publications.countries_fingerprint AS countries_fingerprint,
+ CTE_publications.year AS year,
+ CTE_publications.publisher AS publisher,
+ CTE_publications.original_title AS original_title,
+ CTE_authors_distinct.original_authors AS original_authors,
+ CTE_translators_distinct.translators AS authors,
+ CTE_publications.translated_book_fingerprint AS translated_book_fingerprint
+ FROM
+ CTE_publications
+ INNER JOIN
+ CTE_authors_distinct
+ ON CTE_publications.id = CTE_authors_distinct.id
+ INNER JOIN
+ CTE_translators_distinct
+ ON CTE_publications.id = CTE_translators_distinct.id
+ INNER JOIN
+ CTE_countries_distinct
+ ON CTE_publications.id = CTE_countries_distinct.id
+ GROUP BY
+ CTE_publications.id,
+ CTE_publications.title,
+ CTE_countries_distinct.countries,
+ CTE_publications.year,
+ CTE_publications.publisher,
+ CTE_publications.original_title,
+ CTE_authors_distinct.original_authors,
+ CTE_translators_distinct.translators,
+ CTE_publications.translated_book_fingerprint,
+ CTE_publications.countries_fingerprint;
+ "
+
+ execute "--Up
+ ",
+ "--Down
+
+ DROP VIEW flat_publications
+ "
+
+ execute "--Up
+
+ CREATE MATERIALIZED VIEW search_documents AS
+ SELECT
+ id,
+ to_tsvector('simple'::regconfig, title) ||
+ to_tsvector('simple'::regconfig, countries) ||
+ to_tsvector('simple'::regconfig, publishers) ||
+ to_tsvector('simple'::regconfig, year::text) ||
+ to_tsvector('simple'::regconfig, authors) ||
+ to_tsvector('simple'::regconfig, original_title) ||
+ to_tsvector('simple'::regconfig, original_authors) AS document
+ FROM
+ flat_publications;
+
+ ",
+ "--Down
+
+ DROP MATERIALIZED VIEW search_documents;
+ "
+
+ execute "-- Up
+ ",
+ "-- Down
+
+ ALTER TABLE publications
+ ALTER COLUMN publisher SET NOT NULL;
+ "
+
+ execute "-- Up
+ ",
+ "-- Down
+
+ UPDATE publications
+ SET publisher = (
+ SELECT
+ string_agg(publishers.name, ', ' ORDER BY publishers.name)
+ FROM
+ publishers
+ INNER JOIN
+ publication_publishers
+ ON
+ publishers.id = publication_publishers.publisher_id
+ WHERE
+ publication_publishers.publication_id = publications.id
+ GROUP BY
+ publication_publishers.publication_id
+ );
+ "
+
+ execute "-- Up
+
+ ALTER TABLE publications
+ DROP COLUMN publisher;
+ ",
+ "-- Down
+
+ ALTER TABLE publications
+ ADD publisher VARCHAR;
+ "
+ end
+end
diff --git a/backend/test/richard_burton/flat_publication_test.exs b/backend/test/richard_burton/flat_publication_test.exs
index d723dddb..1f2339a3 100644
--- a/backend/test/richard_burton/flat_publication_test.exs
+++ b/backend/test/richard_burton/flat_publication_test.exs
@@ -13,7 +13,7 @@ defmodule RichardBurton.FlatPublicationTest do
"title" => "Manuel de Moraes: A Chronicle of the Seventeenth Century",
"countries" => "GB",
"year" => 1886,
- "publisher" => "Bickers & Son",
+ "publishers" => "Bickers & Son",
"authors" => "Richard Burton, Isabel Burton",
"original_authors" => "J. M. Pereira da Silva",
"original_title" => "Manuel de Moraes: crônica do século XVII"
@@ -35,7 +35,7 @@ defmodule RichardBurton.FlatPublicationTest do
title: :required,
countries: :required,
year: :required,
- publisher: :required,
+ publishers: :required,
authors: :required,
original_authors: :required,
original_title: :required
@@ -66,12 +66,12 @@ defmodule RichardBurton.FlatPublicationTest do
refute change_valid(%{"title" => nil}).valid?
end
- test "when publisher is blank, is invalid" do
- refute change_valid(%{"publisher" => ""}).valid?
+ test "when publishers is blank, is invalid" do
+ refute change_valid(%{"publishers" => ""}).valid?
end
- test "when publisher is nil, is invalid" do
- refute change_valid(%{"publisher" => nil}).valid?
+ test "when publishers is nil, is invalid" do
+ refute change_valid(%{"publishers" => nil}).valid?
end
test "when year is nil, is invalid" do
diff --git a/backend/test/richard_burton/publication_codec_test.exs b/backend/test/richard_burton/publication_codec_test.exs
index 014eba8c..41f36af9 100644
--- a/backend/test/richard_burton/publication_codec_test.exs
+++ b/backend/test/richard_burton/publication_codec_test.exs
@@ -8,6 +8,7 @@ defmodule RichardBurton.Publication.CodecTest do
alias RichardBurton.Author
alias RichardBurton.Country
alias RichardBurton.Publication
+ alias RichardBurton.Publisher
alias RichardBurton.FlatPublication
alias RichardBurton.TranslatedBook
alias RichardBurton.OriginalBook
@@ -22,7 +23,7 @@ defmodule RichardBurton.Publication.CodecTest do
"countries" => "GB",
"original_authors" => "José de Alencar",
"original_title" => "Iracema",
- "publisher" => "Bickers & Son",
+ "publishers" => "Bickers & Son",
"title" => "Iraçéma the Honey-Lips: A Legend of Brazil",
"year" => "1886"
},
@@ -31,7 +32,7 @@ defmodule RichardBurton.Publication.CodecTest do
"countries" => "US, GB",
"original_authors" => "José de Alencar",
"original_title" => "Ubirajara",
- "publisher" => "Ronald Massey",
+ "publishers" => "Ronald Massey",
"title" => "Ubirajara: A Legend of the Tupy Indians",
"year" => "1922"
},
@@ -40,7 +41,7 @@ defmodule RichardBurton.Publication.CodecTest do
"countries" => "GB",
"original_authors" => "José de Alencar",
"original_title" => "Iracema",
- "publisher" => "Bickers & Son",
+ "publishers" => "Bickers & Son",
"title" => "",
"year" => "AAAA"
},
@@ -49,7 +50,7 @@ defmodule RichardBurton.Publication.CodecTest do
"countries" => "",
"original_authors" => "",
"original_title" => "",
- "publisher" => "",
+ "publishers" => "",
"title" => "Ubirajara: A Legend of the Tupy Indians",
"year" => ""
}
@@ -69,7 +70,7 @@ defmodule RichardBurton.Publication.CodecTest do
"countries" => "GB",
"original_authors" => "José de Alencar",
"original_title" => "Iracema",
- "publisher" => "",
+ "publishers" => "",
"title" => "Iraçéma the Honey-Lips: A Legend of Brazil",
"year" => "1886"
},
@@ -78,7 +79,7 @@ defmodule RichardBurton.Publication.CodecTest do
"countries" => "Ubirajara",
"original_authors" => "",
"original_title" => "Ubirajara: A Legend of the Tupy Indians",
- "publisher" => "",
+ "publishers" => "",
"title" => "J. T. W. Sadler",
"year" => "GB"
},
@@ -88,7 +89,7 @@ defmodule RichardBurton.Publication.CodecTest do
"original_authors" =>
"José de Alencar,1886,GB,Iracema,Iraçéma the Honey-Lips: A Legend of Brazil,Isabel Burton,Bickers & Son",
"original_title" => "",
- "publisher" => "",
+ "publishers" => "",
"title" => "",
"year" => ""
}
@@ -113,7 +114,7 @@ defmodule RichardBurton.Publication.CodecTest do
"title" => "Iraçéma the Honey-Lips: A Legend of Brazil",
"year" => "1886",
"countries" => "GB",
- "publisher" => "Bickers & Son",
+ "publishers" => "Bickers & Son, Noonday Press",
"authors" => "Isabel Burton",
"original_authors" => "José de Alencar",
"original_title" => "Iracema"
@@ -124,7 +125,8 @@ defmodule RichardBurton.Publication.CodecTest do
year: 1886,
countries: "GB",
countries_fingerprint: "B4043B0B8297E379BC559AB33B6AE9C7A9B4EF6519D3BAEE53270F0C0DD3D960",
- publisher: "Bickers & Son",
+ publishers: "Bickers & Son, Noonday Press",
+ publishers_fingerprint: "74A2D52E4AC165134F6A85A9A109A3459B37B557C2D3939D33F02821A8D8A97D",
authors: "Isabel Burton",
original_authors: "José de Alencar",
original_title: "Iracema",
@@ -137,7 +139,7 @@ defmodule RichardBurton.Publication.CodecTest do
"title" => "Iraçéma the Honey-Lips: A Legend of Brazil",
"year" => "1886",
"countries" => "GB",
- "publisher" => "Bickers & Son",
+ "publishers" => [%{"name" => "Bickers & Son"}, %{"name" => "Noonday Press"}],
"translated_book" => %{
"authors" => [
%{"name" => "Isabel Burton"}
@@ -158,8 +160,8 @@ defmodule RichardBurton.Publication.CodecTest do
input = %{
title: "Iraçéma the Honey-Lips: A Legend of Brazil",
year: "1886",
- countries: [%Country{code: "GB"}],
- publisher: "Bickers & Son",
+ countries: [%{code: "GB"}],
+ publishers: [%{name: "Bickers & Son"}, %{name: "Noonday Press"}],
translated_book: %{
authors: [
%{name: "Isabel Burton"}
@@ -181,7 +183,7 @@ defmodule RichardBurton.Publication.CodecTest do
title: "Iraçéma the Honey-Lips: A Legend of Brazil",
year: "1886",
countries: [%Country{code: "GB"}],
- publisher: "Bickers & Son",
+ publishers: [%Publisher{name: "Bickers & Son"}, %Publisher{name: "Noonday Press"}],
translated_book: %TranslatedBook{
authors: [%Author{name: "Isabel Burton"}],
original_book: %OriginalBook{
@@ -200,7 +202,7 @@ defmodule RichardBurton.Publication.CodecTest do
"title" => "Iraçéma the Honey-Lips: A Legend of Brazil",
"year" => "1886",
"countries" => [%{"code" => "GB"}],
- "publisher" => "Bickers & Son",
+ "publishers" => [%{"name" => "Bickers & Son"}, %{"name" => "Noonday Press"}],
"translated_book" => %{
"authors" => [
%{"name" => "Isabel Burton"}
@@ -217,7 +219,7 @@ defmodule RichardBurton.Publication.CodecTest do
title: "Iraçéma the Honey-Lips: A Legend of Brazil",
year: "1886",
countries: [%{code: "GB"}],
- publisher: "Bickers & Son",
+ publishers: [%{name: "Bickers & Son"}, %{name: "Noonday Press"}],
translated_book: %{
authors: [
%{name: "Isabel Burton"}
@@ -234,7 +236,7 @@ defmodule RichardBurton.Publication.CodecTest do
title: "Iraçéma the Honey-Lips: A Legend of Brazil",
year: "1886",
countries: [%Country{code: "GB"}],
- publisher: "Bickers & Son",
+ publishers: [%Publisher{name: "Bickers & Son"}, %Publisher{name: "Noonday Press"}],
translated_book: %TranslatedBook{
authors: [
%Author{name: "Isabel Burton"}
@@ -258,7 +260,7 @@ defmodule RichardBurton.Publication.CodecTest do
"title" => "Ubirajara: A Legend of the Tupy Indians",
"year" => "",
"countries" => "",
- "publisher" => "",
+ "publishers" => "",
"authors" => "J. T. W. Sadler",
"original_authors" => "",
"original_title" => ""
@@ -269,7 +271,7 @@ defmodule RichardBurton.Publication.CodecTest do
"title" => "Ubirajara: A Legend of the Tupy Indians",
"year" => "",
"countries" => "",
- "publisher" => "",
+ "publishers" => "",
"translated_book" => %{
"authors" => [
%{"name" => "J. T. W. Sadler"}
@@ -289,7 +291,7 @@ defmodule RichardBurton.Publication.CodecTest do
errors: %{
year: "required",
countries: "required",
- publisher: "required",
+ publishers: "required",
translated_book: %{
original_book: %{
authors: [%{name: "required"}],
@@ -314,7 +316,7 @@ defmodule RichardBurton.Publication.CodecTest do
"errors" => %{
"year" => "required",
"countries" => "required",
- "publisher" => "required",
+ "publishers" => "required",
"original_authors" => "required",
"original_title" => "required"
}
@@ -338,7 +340,7 @@ defmodule RichardBurton.Publication.CodecTest do
"title" => "Iraçéma the Honey-Lips: A Legend of Brazil",
"year" => "1886",
"countries" => [%{"code" => "GB"}],
- "publisher" => "Bickers & Son",
+ "publishers" => [%{"name" => "Bickers & Son"}, %{"name" => "Noonday Press"}],
"translated_book" => %{
"authors" => [
%{"name" => "Isabel Burton"}
@@ -357,7 +359,8 @@ defmodule RichardBurton.Publication.CodecTest do
year: 1886,
countries: [%Country{code: "GB"}],
countries_fingerprint: "B4043B0B8297E379BC559AB33B6AE9C7A9B4EF6519D3BAEE53270F0C0DD3D960",
- publisher: "Bickers & Son",
+ publishers: [%Publisher{name: "Bickers & Son"}, %Publisher{name: "Noonday Press"}],
+ publishers_fingerprint: "74A2D52E4AC165134F6A85A9A109A3459B37B557C2D3939D33F02821A8D8A97D",
translated_book: %TranslatedBook{
authors: [
%Author{name: "Isabel Burton"}
@@ -377,12 +380,12 @@ defmodule RichardBurton.Publication.CodecTest do
"954F4C8E5EB33960B733BADB84134970AF5D970879260138C8C214B66DDBEF1F"
}
- test "on a nested publication-like with string keys, returns the flattened representation with string keys" do
+ test "on a flat-publication-like with string keys, returns the nested representation with string keys" do
input = %{
"title" => "Iraçéma the Honey-Lips: A Legend of Brazil",
"year" => "1886",
"countries" => "GB",
- "publisher" => "Bickers & Son",
+ "publishers" => "Bickers & Son, Noonday Press",
"authors" => "Isabel Burton",
"original_authors" => "José de Alencar",
"original_title" => "Iracema"
@@ -391,12 +394,12 @@ defmodule RichardBurton.Publication.CodecTest do
assert @output == Publication.Codec.nest(input)
end
- test "on a nested publication-like with atom keys, returns the flattened representation with string keys" do
+ test "on a flat-publication-like with atom keys, returns the nested representation with string keys" do
input = %{
title: "Iraçéma the Honey-Lips: A Legend of Brazil",
year: "1886",
countries: "GB",
- publisher: "Bickers & Son",
+ publishers: "Bickers & Son, Noonday Press",
authors: "Isabel Burton",
original_authors: "José de Alencar",
original_title: "Iracema"
@@ -405,12 +408,12 @@ defmodule RichardBurton.Publication.CodecTest do
assert @output == Publication.Codec.nest(input)
end
- test "on a Publication struct, returns the flattened representation with string keys" do
+ test "on a FlatPublication struct, returns the nested representation with string keys" do
input = %FlatPublication{
title: "Iraçéma the Honey-Lips: A Legend of Brazil",
year: "1886",
countries: "GB",
- publisher: "Bickers & Son",
+ publishers: "Bickers & Son, Noonday Press",
authors: "Isabel Burton",
original_authors: "José de Alencar",
original_title: "Iracema"
@@ -419,13 +422,13 @@ defmodule RichardBurton.Publication.CodecTest do
assert @output_struct == Publication.Codec.nest(input)
end
- test "on a list, returns the flattened representation of its items, with string keys" do
+ test "on a list, returns the nested representation of its items, with string keys" do
input = [
%{
"title" => "Iraçéma the Honey-Lips: A Legend of Brazil",
"year" => "1886",
"countries" => "GB",
- "publisher" => "Bickers & Son",
+ "publishers" => "Bickers & Son, Noonday Press",
"authors" => "Isabel Burton",
"original_authors" => "José de Alencar",
"original_title" => "Iracema"
@@ -434,7 +437,7 @@ defmodule RichardBurton.Publication.CodecTest do
title: "Iraçéma the Honey-Lips: A Legend of Brazil",
year: "1886",
countries: "GB",
- publisher: "Bickers & Son",
+ publishers: "Bickers & Son, Noonday Press",
authors: "Isabel Burton",
original_authors: "José de Alencar",
original_title: "Iracema"
@@ -443,7 +446,7 @@ defmodule RichardBurton.Publication.CodecTest do
title: "Iraçéma the Honey-Lips: A Legend of Brazil",
year: "1886",
countries: "GB",
- publisher: "Bickers & Son",
+ publishers: "Bickers & Son, Noonday Press",
authors: "Isabel Burton",
original_authors: "José de Alencar",
original_title: "Iracema"
diff --git a/backend/test/richard_burton/publication_index_test.exs b/backend/test/richard_burton/publication_index_test.exs
index 1bfe4f85..2e1806f2 100644
--- a/backend/test/richard_burton/publication_index_test.exs
+++ b/backend/test/richard_burton/publication_index_test.exs
@@ -16,7 +16,8 @@ defmodule RichardBurton.Publication.IndexTest do
countries_fingerprint: "4B650E5C4785025DEE7BD65E3C5C527356717D7A1C0BFEF5B4ADA8CA1E9CBE17",
original_authors: "Cyro dos Anjos",
original_title: "O amanuense Belmiro",
- publisher: "Fairleigh Dickinson University Press",
+ publishers: "Fairleigh Dickinson University Press",
+ publishers_fingerprint: "BDBBE0C6ACE0F5D7CDAC2301CBD7DDE19808618AF03AB6B6546FF30A82F4FA5E",
title: "Diary of a Civil Servant",
year: 1986
},
@@ -26,7 +27,8 @@ defmodule RichardBurton.Publication.IndexTest do
countries_fingerprint: "B4043B0B8297E379BC559AB33B6AE9C7A9B4EF6519D3BAEE53270F0C0DD3D960",
original_authors: "Cyro dos Anjos",
original_title: "O amanuense Belmiro",
- publisher: "Associated University Presses",
+ publishers: "Associated University Presses",
+ publishers_fingerprint: "FA1B59EB992D97EB10B6219661EA4C9C740D509048CC0DF9A86EB3BC8EB8E45B",
title: "Diary of a Civil Servant",
year: 1988
},
@@ -36,7 +38,8 @@ defmodule RichardBurton.Publication.IndexTest do
countries_fingerprint: "F060274D35CC0709781F13A9331376B035C9A04546FE43381BC5749F1362C8BF",
original_authors: "Rachel de Queiroz",
original_title: "Dora Doralina",
- publisher: "Dutton",
+ publishers: "Dutton",
+ publishers_fingerprint: "289485905D12E66D52118BCFECB6C911B1A8E4379477DD98ED30F2ED795E260C",
title: "Dora Doralina",
year: 1984
},
@@ -46,7 +49,8 @@ defmodule RichardBurton.Publication.IndexTest do
countries_fingerprint: "BBAF8352442730E92C16C5EA6B0FF7CC595C24E02D8E8BFC5FEA5A4E0BB0B46B",
original_authors: "Machado de Assis",
original_title: "Memórias póstumas de Brás Cubas",
- publisher: "Instituto Nacional do Livro",
+ publishers: "Instituto Nacional do Livro",
+ publishers_fingerprint: "CFC153F1AB2F32958A66F3F4B36EECFFDF8A28C48F202DE09FFEFF6BE98F1027",
title: "Posthumous Reminiscences of Brás Cubas",
year: 1955
},
@@ -56,7 +60,8 @@ defmodule RichardBurton.Publication.IndexTest do
countries_fingerprint: "9B202ECBC6D45C6D8901D989A918878397A3EB9D00E8F48022FC051B19D21A1D",
original_authors: "Rachel de Queiroz",
original_title: "As três Marias",
- publisher: "University of Texas Press",
+ publishers: "University of Texas Press",
+ publishers_fingerprint: "2F6FE554F3CF1014B2345ADE7C06166EA58D929FBEE633D4A782126F5C4331EA",
title: "The Three Marias",
year: 1963
},
@@ -66,7 +71,8 @@ defmodule RichardBurton.Publication.IndexTest do
countries_fingerprint: "9B202ECBC6D45C6D8901D989A918878397A3EB9D00E8F48022FC051B19D21A1D",
original_authors: "Machado de Assis",
original_title: "Memórias póstumas de Brás Cubas",
- publisher: "Oxford University Press",
+ publishers: "Oxford University Press",
+ publishers_fingerprint: "27E4CE2B302408251962F38DD2928A99EB212A7BB09088BBBE6F77944A11A90D",
title: "Posthumous Memoirs of Bras Cubas",
year: 1997
},
@@ -76,7 +82,8 @@ defmodule RichardBurton.Publication.IndexTest do
countries_fingerprint: "B4043B0B8297E379BC559AB33B6AE9C7A9B4EF6519D3BAEE53270F0C0DD3D960",
original_authors: "Erico Verissimo",
original_title: "Olhai os lírios do campo",
- publisher: "Greenwood",
+ publishers: "Greenwood",
+ publishers_fingerprint: "37AA9A83218BF5F4A5F6EABA530C07E64316BA03788B42D0A2A419719B8B12BC",
title: "Consider the Lilies of the Field",
year: 1969
},
@@ -86,7 +93,8 @@ defmodule RichardBurton.Publication.IndexTest do
countries_fingerprint: "9B202ECBC6D45C6D8901D989A918878397A3EB9D00E8F48022FC051B19D21A1D",
original_authors: "Graciliano Ramos",
original_title: "Angústia",
- publisher: "Alfred A. Knopf",
+ publishers: "Alfred A. Knopf",
+ publishers_fingerprint: "A4AFA4682BC9F658DD5DAD7649822F925C9A4FB0A72459F631FA32D07CC405D4",
title: "Anguish",
year: 1946
},
@@ -96,7 +104,8 @@ defmodule RichardBurton.Publication.IndexTest do
countries_fingerprint: "B4043B0B8297E379BC559AB33B6AE9C7A9B4EF6519D3BAEE53270F0C0DD3D960",
original_authors: "Erico Verissimo",
original_title: "O tempo e o vento",
- publisher: "Arco Publications",
+ publishers: "Arco Publications",
+ publishers_fingerprint: "DD6D4A5F8B8C4DD9BB9E5AD5634BB98CC3568E943729417FD69846D75C07B802",
title: "Time and the Wind",
year: 1954
},
@@ -106,7 +115,8 @@ defmodule RichardBurton.Publication.IndexTest do
countries_fingerprint: "B4043B0B8297E379BC559AB33B6AE9C7A9B4EF6519D3BAEE53270F0C0DD3D960",
original_authors: "Erico Verissimo",
original_title: "Noite",
- publisher: "Arco Publications",
+ publishers: "Arco Publications",
+ publishers_fingerprint: "DD6D4A5F8B8C4DD9BB9E5AD5634BB98CC3568E943729417FD69846D75C07B802",
title: "Night",
year: 1956
},
@@ -116,7 +126,8 @@ defmodule RichardBurton.Publication.IndexTest do
countries_fingerprint: "9B202ECBC6D45C6D8901D989A918878397A3EB9D00E8F48022FC051B19D21A1D",
original_authors: "Erico Verissimo",
original_title: "O tempo e o vento",
- publisher: "Macmillan",
+ publishers: "Macmillan",
+ publishers_fingerprint: "873D23F97EEB8B04973339EC8A202DC8AEC0B33298D2E194301E223ECD7E9C05",
title: "Time and the Wind",
year: 1951
},
@@ -126,7 +137,8 @@ defmodule RichardBurton.Publication.IndexTest do
countries_fingerprint: "9B202ECBC6D45C6D8901D989A918878397A3EB9D00E8F48022FC051B19D21A1D",
original_authors: "Erico Verissimo",
original_title: "Noite",
- publisher: "Macmillan",
+ publishers: "Macmillan",
+ publishers_fingerprint: "873D23F97EEB8B04973339EC8A202DC8AEC0B33298D2E194301E223ECD7E9C05",
title: "Night",
year: 1956
},
@@ -136,7 +148,8 @@ defmodule RichardBurton.Publication.IndexTest do
countries_fingerprint: "9B202ECBC6D45C6D8901D989A918878397A3EB9D00E8F48022FC051B19D21A1D",
original_authors: "Erico Verissimo",
original_title: "O senhor embaixador",
- publisher: "Macmillan",
+ publishers: "Macmillan",
+ publishers_fingerprint: "873D23F97EEB8B04973339EC8A202DC8AEC0B33298D2E194301E223ECD7E9C05",
title: "His Excellency, the Ambassador",
year: 1967
},
@@ -146,7 +159,8 @@ defmodule RichardBurton.Publication.IndexTest do
countries_fingerprint: "9B202ECBC6D45C6D8901D989A918878397A3EB9D00E8F48022FC051B19D21A1D",
original_authors: "Mário de Andrade",
original_title: "Amar verbo intransitivo",
- publisher: "MacCaulay",
+ publishers: "MacCaulay",
+ publishers_fingerprint: "A092A747DE2B957ADC822F5FEE63B2078F4CEE237438789BBF9A6D10F9F104E1",
title: "Fraulein",
year: 1933
},
@@ -156,7 +170,8 @@ defmodule RichardBurton.Publication.IndexTest do
countries_fingerprint: "9B202ECBC6D45C6D8901D989A918878397A3EB9D00E8F48022FC051B19D21A1D",
original_authors: "Graciliano Ramos",
original_title: "Memórias do cárcere",
- publisher: "Evans",
+ publishers: "Evans",
+ publishers_fingerprint: "8658EBF1EDF525094102102EB55229187C236F9147950C775273B2D33AF516F0",
title: "Jail Prison Memoirs",
year: 1974
},
@@ -166,7 +181,8 @@ defmodule RichardBurton.Publication.IndexTest do
countries_fingerprint: "B4043B0B8297E379BC559AB33B6AE9C7A9B4EF6519D3BAEE53270F0C0DD3D960",
original_authors: "Machado de Assis",
original_title: "Memórias póstumas de Brás Cubas",
- publisher: "W.H. Allen",
+ publishers: "W.H. Allen",
+ publishers_fingerprint: "0AE69A42F21F227103D46FF569A689AC1A139BD3F036C74DEA48E8C86FF93326",
title: "Epitaph of a Small Winner",
year: 1953
},
@@ -176,7 +192,8 @@ defmodule RichardBurton.Publication.IndexTest do
countries_fingerprint: "9B202ECBC6D45C6D8901D989A918878397A3EB9D00E8F48022FC051B19D21A1D",
original_authors: "Machado de Assis",
original_title: "Memórias póstumas de Brás Cubas",
- publisher: "Noonday Press",
+ publishers: "Noonday Press",
+ publishers_fingerprint: "3444E1379BFB654A280E4E86B4BD0916534828F1AA529FFB8714D315E203F166",
title: "Epitaph of a Small Winner",
year: 1952
}
@@ -378,7 +395,7 @@ defmodule RichardBurton.Publication.IndexTest do
assert_search_results(
publications,
expect: [
- publisher: expected_publishers
+ publishers: expected_publishers
]
)
end
diff --git a/backend/test/richard_burton/publication_test.exs b/backend/test/richard_burton/publication_test.exs
index 40d2bc03..8a35520d 100644
--- a/backend/test/richard_burton/publication_test.exs
+++ b/backend/test/richard_burton/publication_test.exs
@@ -14,7 +14,7 @@ defmodule RichardBurton.PublicationTest do
"title" => "Manuel de Moraes: A Chronicle of the Seventeenth Century",
"countries" => [%{"code" => "GB"}],
"year" => 1886,
- "publisher" => "Bickers & Son",
+ "publishers" => [%{"name" => "Bickers & Son"}],
"translated_book" => %{
"authors" => [
%{"name" => "Richard Burton"},
@@ -36,7 +36,7 @@ defmodule RichardBurton.PublicationTest do
title: :required,
countries: :required,
year: :required,
- publisher: :required,
+ publishers: :required,
translated_book: :required
}
@@ -44,7 +44,7 @@ defmodule RichardBurton.PublicationTest do
title: :required,
countries: :required,
year: :required,
- publisher: :required,
+ publishers: :required,
translated_book: %{
authors: :required,
original_book: %{authors: :required, title: :required}
@@ -96,12 +96,12 @@ defmodule RichardBurton.PublicationTest do
refute change_valid(%{"countries" => "XX"}).valid?
end
- test "when publisher is blank, is invalid" do
- refute change_valid(%{"publisher" => ""}).valid?
+ test "when publishers is blank, is invalid" do
+ refute change_valid(%{"publishers" => ""}).valid?
end
- test "when publisher is nil, is invalid" do
- refute change_valid(%{"publisher" => nil}).valid?
+ test "when publishers is nil, is invalid" do
+ refute change_valid(%{"publishers" => nil}).valid?
end
test "when year is nil, is invalid" do
diff --git a/backend/test/richard_burton/publisher_test.exs b/backend/test/richard_burton/publisher_test.exs
new file mode 100644
index 00000000..0fab340b
--- /dev/null
+++ b/backend/test/richard_burton/publisher_test.exs
@@ -0,0 +1,236 @@
+defmodule RichardBurton.PublisherTest do
+ @moduledoc """
+ Tests for the Publisher schema
+ """
+ use RichardBurton.DataCase
+
+ alias RichardBurton.Publisher
+ alias RichardBurton.Validation
+ alias RichardBurton.Util
+
+ defmodule WithManyPublishers do
+ use Ecto.Schema
+ import Ecto.Changeset
+
+ schema "with_many_publishers" do
+ field(:publishers_fingerprint, :string)
+ has_many :publishers, Publisher
+ end
+
+ def changeset(attrs) do
+ %WithManyPublishers{} |> cast(attrs, []) |> cast_assoc(:publishers)
+ end
+ end
+
+ @valid_attrs %{
+ "name" => "Noonday Press"
+ }
+
+ defp changeset(attrs = %{}) do
+ Publisher.changeset(%Publisher{}, attrs)
+ end
+
+ defp change_valid(attrs = %{}) do
+ changeset(Util.deep_merge_maps(@valid_attrs, attrs))
+ end
+
+ defp insert(attrs) do
+ %Publisher{} |> Publisher.changeset(attrs) |> Repo.insert()
+ end
+
+ defp insert!(attrs) do
+ attrs |> changeset() |> Repo.insert!()
+ end
+
+ defp get_publishers(changeset = %Ecto.Changeset{}) do
+ changeset
+ |> get_change(:publishers)
+ |> Enum.map(&apply_changes/1)
+ end
+
+ defp get_fingerprint(changeset = %Ecto.Changeset{}) do
+ get_change(changeset, :publishers_fingerprint)
+ end
+
+ describe "changeset/2" do
+ test "when valid attributes are provided, is valid" do
+ assert changeset(@valid_attrs).valid?
+ end
+
+ test "when name is blank, is invalid" do
+ refute change_valid(%{"name" => ""}).valid?
+ end
+
+ test "when name is nil, is invalid" do
+ refute change_valid(%{"name" => nil}).valid?
+ end
+
+ test "when name is a non-blank string, is valid" do
+ assert change_valid(%{"name" => "Bickers & SonA"}).valid?
+ end
+
+ test "when a publisher with the provided attributes already exists, is invalid" do
+ {:ok, _} = insert(@valid_attrs)
+ {:error, changeset} = insert(@valid_attrs)
+
+ refute changeset.valid?
+ assert :conflict == Validation.get_errors(changeset)
+ end
+ end
+
+ describe "maybe_insert/1" do
+ test "when there is no publisher with the provided name, inserts it" do
+ publisher = Publisher.maybe_insert!(@valid_attrs)
+
+ assert [publisher] == Publisher.all()
+ end
+
+ test "when there is a publisher with the provided name, returns the pre-existent one" do
+ insert(@valid_attrs)
+ assert [preexistent_publisher] = Publisher.all()
+
+ publisher = Publisher.maybe_insert!(@valid_attrs)
+
+ assert preexistent_publisher == publisher
+ assert [publisher] == Publisher.all()
+ end
+ end
+
+ describe "fingerprint/1" do
+ test "given two different lists of publishers, generates different fingerprints" do
+ publishers1 = [%Publisher{name: "Noonday Press"}, %Publisher{name: "Bickers & Son"}]
+ publishers2 = [%Publisher{name: "Bickers & Son"}, %Publisher{name: "IE"}]
+
+ refute Publisher.fingerprint(publishers1) == Publisher.fingerprint(publishers2)
+ end
+
+ test "given two lists of publishers with the same names, generates the same fingerprints" do
+ publishers1 = [%Publisher{name: "Noonday Press"}, %Publisher{name: "Bickers & Son"}]
+ publishers2 = [%Publisher{name: "Noonday Press"}, %Publisher{name: "Bickers & Son"}]
+
+ assert Publisher.fingerprint(publishers1) == Publisher.fingerprint(publishers2)
+ end
+
+ test "given two lists of publishers with the same and different order, generates the same fingerprints" do
+ publishers1 = [%Publisher{name: "Noonday Press"}, %Publisher{name: "Bickers & Son"}]
+ publishers2 = [%Publisher{name: "Bickers & Son"}, %Publisher{name: "Noonday Press"}]
+
+ assert Publisher.fingerprint(publishers1) == Publisher.fingerprint(publishers2)
+ end
+ end
+
+ describe "link/1" do
+ test "links existing publishers to changeset" do
+ attrs = %{
+ "publishers" => [
+ %{"name" => "Noonday Press"},
+ %{"name" => "Bickers & Son"}
+ ]
+ }
+
+ publishers = Enum.map(attrs["publishers"], &insert!/1)
+
+ changeset =
+ attrs
+ |> WithManyPublishers.changeset()
+ |> Publisher.link()
+
+ assert changeset.valid?
+ assert publishers == get_publishers(changeset)
+ end
+
+ test "links non-existing publishers to changeset, inserting them" do
+ attrs = %{
+ "publishers" => [
+ %{"name" => "Noonday Press"},
+ %{"name" => "Bickers & Son"}
+ ]
+ }
+
+ changeset =
+ attrs
+ |> WithManyPublishers.changeset()
+ |> Publisher.link()
+
+ assert changeset.valid?
+ assert Publisher.all() == get_publishers(changeset)
+ end
+
+ test "has no side effects when changeset is invalid" do
+ changeset =
+ %{"publishers" => [%{}]}
+ |> WithManyPublishers.changeset()
+ |> Publisher.link()
+
+ refute changeset.valid?
+ assert Enum.empty?(Publisher.all())
+ end
+ end
+
+ describe "link_fingerprint/1" do
+ test "links fingerprint Bickers & Soning publisher names to changeset" do
+ attrs = %{
+ "publishers" => [
+ %{"name" => "Noonday Press"},
+ %{"name" => "Bickers & Son"}
+ ]
+ }
+
+ changeset =
+ attrs
+ |> WithManyPublishers.changeset()
+ |> Publisher.link_fingerprint()
+
+ assert changeset.valid?
+ assert Publisher.fingerprint(get_publishers(changeset)) == get_fingerprint(changeset)
+ end
+
+ test "does not link fingerprint to invalid changeset" do
+ changeset =
+ %{"publishers" => [%{}]}
+ |> WithManyPublishers.changeset()
+ |> Publisher.link_fingerprint()
+
+ refute changeset.valid?
+ assert is_nil(get_fingerprint(changeset))
+ end
+ end
+
+ describe "flatten/1" do
+ test "with a list of maps with string keys, returns a list of maps with name key" do
+ publishers = [
+ %{"name" => "Noonday Press"},
+ %{"name" => "Bickers & Son"}
+ ]
+
+ assert "Noonday Press, Bickers & Son" = Publisher.flatten(publishers)
+ end
+
+ test "with a list of maps with atom keys, returns a list of maps with name key" do
+ publishers = [
+ %{name: "Noonday Press"},
+ %{name: "Bickers & Son"}
+ ]
+
+ assert "Noonday Press, Bickers & Son" = Publisher.flatten(publishers)
+ end
+
+ test "with a list of Publisher structs, returns a list of maps with name key" do
+ publishers = [
+ %Publisher{name: "Noonday Press"},
+ %Publisher{name: "Bickers & Son"}
+ ]
+
+ assert "Noonday Press, Bickers & Son" = Publisher.flatten(publishers)
+ end
+ end
+
+ describe "nest/1" do
+ test "with a comma separated string, returns a list of maps with name key" do
+ publishers = "Noonday Press, Bickers & Son"
+
+ assert [%{"name" => "Noonday Press"}, %{"name" => "Bickers & Son"}] =
+ Publisher.nest(publishers)
+ end
+ end
+end
diff --git a/backend/test/richard_burton/translated_book_test.exs b/backend/test/richard_burton/translated_book_test.exs
index 858ead7f..f7f72ff6 100644
--- a/backend/test/richard_burton/translated_book_test.exs
+++ b/backend/test/richard_burton/translated_book_test.exs
@@ -189,7 +189,7 @@ defmodule RichardBurton.TranslatedBookTest do
"title" => "Manuel de Moraes: A Chronicle of the Seventeenth Century",
"countries" => [%{"code" => "GB"}],
"year" => 1886,
- "publisher" => "Bickers & Son",
+ "publishers" => [%{"name" => "Bickers & Son"}],
"translated_book" => %{
"authors" => [
%{"name" => "Richard Burton"},
diff --git a/backend/test/richard_burton_web/controllers/publication_controller_test.exs b/backend/test/richard_burton_web/controllers/publication_controller_test.exs
index 62b6043f..d53b1e22 100644
--- a/backend/test/richard_burton_web/controllers/publication_controller_test.exs
+++ b/backend/test/richard_burton_web/controllers/publication_controller_test.exs
@@ -11,7 +11,7 @@ defmodule RichardBurtonWeb.PublicationControllerTest do
"title" => "Iraçéma the Honey-Lips: A Legend of Brazil",
"year" => "1886",
"countries" => "GB",
- "publisher" => "Bickers & Son",
+ "publishers" => "Bickers & Son",
"authors" => "Isabel Burton",
"original_authors" => "José de Alencar",
"original_title" => "Iracema"
@@ -37,7 +37,7 @@ defmodule RichardBurtonWeb.PublicationControllerTest do
"title" => "Manuel de Moraes: A Chronicle of the Seventeenth Century",
"countries" => "GB",
"year" => 1886,
- "publisher" => "Bickers & Son",
+ "publishers" => "Bickers & Son",
"authors" => "Richard Burton, Isabel Burton",
"original_authors" => "J. M. Pereira da Silva",
"original_title" => "Manuel de Moraes: crônica do século XVII"
@@ -47,7 +47,7 @@ defmodule RichardBurtonWeb.PublicationControllerTest do
"title" => "Iraçéma the Honey-Lips: A Legend of Brazil",
"year" => 1886,
"countries" => "GB",
- "publisher" => "Bickers & Son",
+ "publishers" => "Bickers & Son",
"authors" => "Isabel Burton",
"original_authors" => "José de Alencar",
"original_title" => "Iracema"
@@ -57,7 +57,7 @@ defmodule RichardBurtonWeb.PublicationControllerTest do
"title" => "",
"year" => "1886",
"countries" => "GB",
- "publisher" => "Bickers & Son",
+ "publishers" => "Bickers & Son",
"authors" => "",
"original_authors" => "José de Alencar",
"original_title" => "Iracema"
@@ -113,7 +113,7 @@ defmodule RichardBurtonWeb.PublicationControllerTest do
"title" => "Iraçéma the Honey-Lips: A Legend of Brazil",
"year" => "1886",
"countries" => "GB",
- "publisher" => "Bickers & Son",
+ "publishers" => "Bickers & Son",
"authors" => "Isabel Burton, Richard Burton",
"original_authors" => "José de Alencar",
"original_title" => "Iracema"
@@ -122,7 +122,7 @@ defmodule RichardBurtonWeb.PublicationControllerTest do
"title" => "Ubirajara: A Legend of the Tupy Indians",
"year" => "1922",
"countries" => "US",
- "publisher" => "Ronald Massey",
+ "publishers" => "Ronald Massey",
"authors" => "J. T. W. Sadler",
"original_authors" => "José de Alencar",
"original_title" => "Ubirajara"
@@ -131,7 +131,7 @@ defmodule RichardBurtonWeb.PublicationControllerTest do
"title" => "",
"year" => "AAAA",
"countries" => "GB",
- "publisher" => "Bickers & Son",
+ "publishers" => "Bickers & Son",
"authors" => "",
"original_authors" => "José de Alencar",
"original_title" => "Iracema"
@@ -140,7 +140,7 @@ defmodule RichardBurtonWeb.PublicationControllerTest do
"title" => "Ubirajara: A Legend of the Tupy Indians",
"year" => "",
"countries" => "",
- "publisher" => "",
+ "publishers" => "",
"authors" => "J. T. W. Sadler",
"original_authors" => "",
"original_title" => ""
@@ -149,7 +149,7 @@ defmodule RichardBurtonWeb.PublicationControllerTest do
"title" => "Ubirajara: A Legend of the Tupy Indians",
"year" => "",
"countries" => "USA",
- "publisher" => "",
+ "publishers" => "",
"authors" => "J. T. W. Sadler",
"original_authors" => "",
"original_title" => ""
@@ -185,7 +185,7 @@ defmodule RichardBurtonWeb.PublicationControllerTest do
"errors" => %{
"year" => "required",
"countries" => "required",
- "publisher" => "required",
+ "publishers" => "required",
"original_authors" => "required",
"original_title" => "required"
}
@@ -195,7 +195,7 @@ defmodule RichardBurtonWeb.PublicationControllerTest do
"errors" => %{
"year" => "required",
"countries" => "alpha2",
- "publisher" => "required",
+ "publishers" => "required",
"original_authors" => "required",
"original_title" => "required"
}
@@ -219,7 +219,7 @@ defmodule RichardBurtonWeb.PublicationControllerTest do
"title" => "Iraçéma the Honey-Lips: A Legend of Brazil",
"year" => "1886",
"countries" => "GB",
- "publisher" => "Bickers & Son",
+ "publishers" => "Bickers & Son",
"authors" => "Isabel Burton, Richard Burton",
"original_authors" => "José de Alencar",
"original_title" => "Iracema"
@@ -231,7 +231,7 @@ defmodule RichardBurtonWeb.PublicationControllerTest do
"title" => "Ubirajara: A Legend of the Tupy Indians",
"year" => "1922",
"countries" => "US, GB",
- "publisher" => "Ronald Massey",
+ "publishers" => "Ronald Massey",
"authors" => "J. T. W. Sadler",
"original_authors" => "José de Alencar",
"original_title" => "Ubirajara"
@@ -243,7 +243,7 @@ defmodule RichardBurtonWeb.PublicationControllerTest do
"title" => "",
"year" => "AAAA",
"countries" => "GB",
- "publisher" => "Bickers & Son",
+ "publishers" => "Bickers & Son",
"authors" => "",
"original_authors" => "José de Alencar",
"original_title" => "Iracema"
@@ -259,7 +259,7 @@ defmodule RichardBurtonWeb.PublicationControllerTest do
"title" => "Ubirajara: A Legend of the Tupy Indians",
"year" => "",
"countries" => "",
- "publisher" => "",
+ "publishers" => "",
"authors" => "J. T. W. Sadler",
"original_authors" => "",
"original_title" => ""
@@ -267,7 +267,7 @@ defmodule RichardBurtonWeb.PublicationControllerTest do
"errors" => %{
"year" => "required",
"countries" => "required",
- "publisher" => "required",
+ "publishers" => "required",
"original_authors" => "required",
"original_title" => "required"
}
@@ -312,7 +312,7 @@ defmodule RichardBurtonWeb.PublicationControllerTest do
conn = get(meta.conn, publication_path(meta.conn, :export))
expected_data =
- "authors;countries;original_authors;original_title;publisher;title;year\nIsabel Burton;GB;José de Alencar;Iracema;Bickers & Son;Iraçéma the Honey-Lips: A Legend of Brazil;1886\n"
+ "authors;countries;original_authors;original_title;publishers;title;year\nIsabel Burton;GB;José de Alencar;Iracema;Bickers & Son;Iraçéma the Honey-Lips: A Legend of Brazil;1886\n"
expected_filename = "publications.csv"
expected_content_disposition = ["attachment; filename=\"#{expected_filename}\""]
@@ -342,7 +342,7 @@ defmodule RichardBurtonWeb.PublicationControllerTest do
conn = get(meta.conn, path)
expected_data =
- "authors;countries;original_authors;original_title;publisher;title;year\nIsabel Burton;GB;José de Alencar;Iracema;Bickers & Son;Iraçéma the Honey-Lips: A Legend of Brazil;1886\n"
+ "authors;countries;original_authors;original_title;publishers;title;year\nIsabel Burton;GB;José de Alencar;Iracema;Bickers & Son;Iraçéma the Honey-Lips: A Legend of Brazil;1886\n"
expected_filename = "publications-#{search}.csv"
expected_content_disposition = ["attachment; filename=\"#{expected_filename}\""]
diff --git a/frontend/components/PublicationIndexList.tsx b/frontend/components/PublicationIndexList.tsx
index 12e7876e..1f17d995 100644
--- a/frontend/components/PublicationIndexList.tsx
+++ b/frontend/components/PublicationIndexList.tsx
@@ -22,7 +22,7 @@ const PublicationItem: FC<{ id: number }> = ({ id }) => {
{publication.originalAuthors}
, published by{" "}
- {publication.publisher}
+ {publication.publishers}
diff --git a/frontend/components/PublicationModal.tsx b/frontend/components/PublicationModal.tsx
index aa373b6a..a0924eba 100644
--- a/frontend/components/PublicationModal.tsx
+++ b/frontend/components/PublicationModal.tsx
@@ -76,7 +76,17 @@ const PublicationDescription: FC<{ publication: Publication }> = ({
label: Publication.describeValue(id, "countries"),
}))}
/>
- in {publication?.year} by .
+ in {publication?.year} by{" "}
+ id.trim())
+ .map((id) => ({
+ id,
+ label: Publication.describeValue(id, "publishers"),
+ }))}
+ />
+ .
);
@@ -87,8 +97,6 @@ const PublicationModal: FC = () => {
const publication = Publication.STORE.usePublication(publicationId);
- console.log({ publication });
-
return (
{publication && (
diff --git a/frontend/modules/publication.ts b/frontend/modules/publication.ts
index 62b4fb55..f6c4818c 100644
--- a/frontend/modules/publication.ts
+++ b/frontend/modules/publication.ts
@@ -26,7 +26,7 @@ type Publication = {
title: string;
countries: string;
year: string;
- publisher: string;
+ publishers: string;
authors: string;
originalTitle: string;
originalAuthors: string;
@@ -183,7 +183,7 @@ const DEFAULT_ATTRIBUTE_VISIBILITY: Record = {
title: true,
countries: true,
year: true,
- publisher: true,
+ publishers: true,
authors: true,
originalTitle: true,
originalAuthors: true,
@@ -384,14 +384,14 @@ const Publication: PublicationModule = {
"authors",
"year",
"countries",
- "publisher",
+ "publishers",
],
ATTRIBUTE_LABELS: {
authors: "Translators",
originalAuthors: "Original Authors",
originalTitle: "Original Title",
countries: "Country",
- publisher: "Publisher",
+ publishers: "Publisher",
title: "Title",
year: "Year",
},
@@ -401,7 +401,7 @@ const Publication: PublicationModule = {
originalAuthors: "array",
originalTitle: "text",
countries: "enumArray",
- publisher: "text",
+ publishers: "array",
title: "text",
year: "number",
},
@@ -411,7 +411,7 @@ const Publication: PublicationModule = {
originalAuthors: true,
originalTitle: false,
countries: true,
- publisher: true,
+ publishers: true,
title: false,
year: true,
},
@@ -903,7 +903,7 @@ const Publication: PublicationModule = {
countries: "",
originalAuthors: "",
originalTitle: "",
- publisher: "",
+ publishers: "",
title: "",
year: "",
};
From f87c7f832cbd3dadbc861e21be16b900f1f3796a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9s=20Vidal?=
Date: Sun, 23 Jun 2024 20:18:09 -0300
Subject: [PATCH 5/8] Add autocomplete to publisher field
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Signed-off-by: Andrés Vidal
---
backend/lib/richard_burton/publisher.ex | 19 +++++
.../controllers/publisher_controller.ex | 13 +++
backend/lib/richard_burton_web/router.ex | 1 +
...3224923_create_publishers_search_index.exs | 14 ++++
.../test/richard_burton/publisher_test.exs | 82 +++++++++++++++++++
.../controllers/publisher_controller_test.exs | 45 ++++++++++
frontend/modules/publication.ts | 4 +
frontend/modules/publisher.ts | 25 ++++++
8 files changed, 203 insertions(+)
create mode 100644 backend/lib/richard_burton_web/controllers/publisher_controller.ex
create mode 100644 backend/priv/repo/migrations/20240623224923_create_publishers_search_index.exs
create mode 100644 backend/test/richard_burton_web/controllers/publisher_controller_test.exs
create mode 100644 frontend/modules/publisher.ts
diff --git a/backend/lib/richard_burton/publisher.ex b/backend/lib/richard_burton/publisher.ex
index 966ee4e0..51e891cb 100644
--- a/backend/lib/richard_burton/publisher.ex
+++ b/backend/lib/richard_burton/publisher.ex
@@ -4,6 +4,7 @@ defmodule RichardBurton.Publisher do
"""
use Ecto.Schema
import Ecto.Changeset
+ import Ecto.Query
alias RichardBurton.Publisher
alias RichardBurton.Repo
@@ -84,6 +85,24 @@ defmodule RichardBurton.Publisher do
def link_fingerprint(changeset = %Ecto.Changeset{valid?: false}), do: changeset
+ @spec search(binary(), :fuzzy | :prefix) :: any()
+ def search(term, :prefix) when is_binary(term) do
+ from(p in Publisher, where: ilike(p.name, ^"#{term}%"))
+ |> Repo.all()
+ end
+
+ def search(term, :fuzzy) when is_binary(term) do
+ from(p in Publisher, where: fragment("similarity((?), (?)) > 0.3", p.name, ^term))
+ |> Repo.all()
+ end
+
+ def search(term) when is_binary(term) do
+ case search(term, :prefix) do
+ [] -> search(term, :fuzzy)
+ keywords when is_list(keywords) -> keywords
+ end
+ end
+
def nest(publishers) when is_binary(publishers) do
publishers |> String.split(",") |> Enum.map(&%{"name" => String.trim(&1)})
end
diff --git a/backend/lib/richard_burton_web/controllers/publisher_controller.ex b/backend/lib/richard_burton_web/controllers/publisher_controller.ex
new file mode 100644
index 00000000..bc5b42ec
--- /dev/null
+++ b/backend/lib/richard_burton_web/controllers/publisher_controller.ex
@@ -0,0 +1,13 @@
+defmodule RichardBurtonWeb.PublisherController do
+ use RichardBurtonWeb, :controller
+
+ alias RichardBurton.Publisher
+
+ def index(conn, %{"search" => query}) do
+ json(conn, Enum.map(Publisher.search(query), &Map.get(&1, :name)))
+ end
+
+ def index(conn, _params) do
+ json(conn, Enum.map(Publisher.all(), &Map.get(&1, :name)))
+ end
+end
diff --git a/backend/lib/richard_burton_web/router.ex b/backend/lib/richard_burton_web/router.ex
index 8fb41e05..6200e60a 100644
--- a/backend/lib/richard_burton_web/router.ex
+++ b/backend/lib/richard_burton_web/router.ex
@@ -38,6 +38,7 @@ defmodule RichardBurtonWeb.Router do
pipe_through(:authorize_admin)
get("/authors", AuthorController, :index)
+ get("/publishers", PublisherController, :index)
scope "/publications" do
post("/bulk", PublicationController, :create_all)
diff --git a/backend/priv/repo/migrations/20240623224923_create_publishers_search_index.exs b/backend/priv/repo/migrations/20240623224923_create_publishers_search_index.exs
new file mode 100644
index 00000000..b089df80
--- /dev/null
+++ b/backend/priv/repo/migrations/20240623224923_create_publishers_search_index.exs
@@ -0,0 +1,14 @@
+defmodule RichardBurton.Repo.Migrations.CreatePublishersSearchIndex do
+ use Ecto.Migration
+
+ def change do
+ execute(
+ "--Up
+ CREATE INDEX publishers_name_trigram_index ON publishers USING gin(name gin_trgm_ops);
+ ",
+ "--Down
+ DROP INDEX publishers_name_trigram_index;
+ "
+ )
+ end
+end
diff --git a/backend/test/richard_burton/publisher_test.exs b/backend/test/richard_burton/publisher_test.exs
index 0fab340b..82652450 100644
--- a/backend/test/richard_burton/publisher_test.exs
+++ b/backend/test/richard_burton/publisher_test.exs
@@ -26,6 +26,14 @@ defmodule RichardBurton.PublisherTest do
"name" => "Noonday Press"
}
+ @publishers [
+ %{"name" => "Random House"},
+ %{"name" => "Bantam Books"},
+ %{"name" => "Dutton"},
+ %{"name" => "UMass Dartmouth"},
+ %{"name" => "University Press of Kentucky"}
+ ]
+
defp changeset(attrs = %{}) do
Publisher.changeset(%Publisher{}, attrs)
end
@@ -52,6 +60,14 @@ defmodule RichardBurton.PublisherTest do
get_change(changeset, :publishers_fingerprint)
end
+ def search_fixture(_) do
+ @publishers
+ |> Enum.map(&Publisher.changeset(%Publisher{}, &1))
+ |> Enum.each(&Repo.insert!/1)
+
+ []
+ end
+
describe "changeset/2" do
test "when valid attributes are provided, is valid" do
assert changeset(@valid_attrs).valid?
@@ -196,6 +212,72 @@ defmodule RichardBurton.PublisherTest do
end
end
+ describe "search/2 when second argument is :prefix" do
+ setup [:search_fixture]
+
+ test "retrieves publishers by full name" do
+ term = "University Press of Kentucky"
+
+ for %Publisher{name: name} <- Publisher.search(term, :prefix) do
+ assert name == term
+ end
+ end
+
+ test "retrieves publishers by prefix" do
+ term = "U"
+
+ for %Publisher{name: name} <- Publisher.search(term, :prefix) do
+ assert name in ["UMass Dartmouth", "University Press of Kentucky"]
+ end
+ end
+ end
+
+ describe "search/2 when second argument is :fuzzy" do
+ setup [:search_fixture]
+
+ test "retrieves publishers by full name" do
+ term = "University Press of Kentucky"
+
+ for %Publisher{name: name} <- Publisher.search(term, :fuzzy) do
+ assert name == term
+ end
+ end
+
+ test "retrieves publishers by similarity" do
+ for %Publisher{name: name} <- Publisher.search("Universiti Press of Kentucky", :fuzzy) do
+ assert name == "University Press of Kentucky"
+ end
+
+ for %Publisher{name: name} <- Publisher.search("Tan", :fuzzy) do
+ assert name in ["Random House", "Bantam Books"]
+ end
+
+ for %Publisher{name: name} <- Publisher.search("Dutan", :fuzzy) do
+ assert name in ["Dutton"]
+ end
+ end
+ end
+
+ describe "search/1" do
+ setup [:search_fixture]
+
+ test "searches by prefix first" do
+ for %Publisher{name: name} <- Publisher.search("Ran") do
+ assert name == "Random House"
+ end
+ end
+
+ test "searches by similarity if prefix search renders no results" do
+ term = "Rant"
+
+ assert [] == Publisher.search(term, :prefix)
+
+ for %Publisher{name: name} <- Publisher.search(term) do
+ assert name in ["Random House", "Bantam Books"]
+ end
+ end
+ end
+
describe "flatten/1" do
test "with a list of maps with string keys, returns a list of maps with name key" do
publishers = [
diff --git a/backend/test/richard_burton_web/controllers/publisher_controller_test.exs b/backend/test/richard_burton_web/controllers/publisher_controller_test.exs
new file mode 100644
index 00000000..556aa520
--- /dev/null
+++ b/backend/test/richard_burton_web/controllers/publisher_controller_test.exs
@@ -0,0 +1,45 @@
+defmodule RichardBurtonWeb.PublisherControllerTest do
+ @moduledoc """
+ Tests for the Publisher controller
+ """
+ use RichardBurtonWeb.ConnCase
+ import Routes, only: [publisher_path: 2]
+
+ alias RichardBurton.Publisher
+ alias RichardBurton.Repo
+
+ @publisher_names [
+ "Random House",
+ "Bantam Books",
+ "Dutton",
+ "UMass Dartmouth",
+ "University Press of Kentucky"
+ ]
+
+ def search_fixture(_) do
+ @publisher_names
+ |> Enum.map(&%{"name" => &1})
+ |> Enum.map(&Publisher.changeset(%Publisher{}, &1))
+ |> Enum.each(&Repo.insert!/1)
+
+ []
+ end
+
+ describe "GET /publishers" do
+ setup [:search_fixture]
+
+ test "returns 200 and all the publishers' names when no search param is provided",
+ %{conn: conn} do
+ expect_auth_authorize_admin()
+ conn = get(conn, publisher_path(conn, :index))
+ assert @publisher_names == json_response(conn, 200)
+ end
+
+ test "returns 200 and the relevant publishers' names a search param is provided",
+ %{conn: conn} do
+ expect_auth_authorize_admin()
+ conn = get(conn, publisher_path(conn, :index), %{"search" => "U"})
+ assert ["UMass Dartmouth", "University Press of Kentucky"] == json_response(conn, 200)
+ end
+ end
+end
diff --git a/frontend/modules/publication.ts b/frontend/modules/publication.ts
index f6c4818c..c4dfa2f2 100644
--- a/frontend/modules/publication.ts
+++ b/frontend/modules/publication.ts
@@ -21,6 +21,7 @@ import {
import useDebounce from "utils/useDebounce";
import { Author } from "./author";
import { COUNTRIES, Country } from "./country";
+import { Publisher } from "./publisher";
type Publication = {
title: string;
@@ -367,6 +368,7 @@ interface PublicationModule {
autocomplete(value: string, attribute: "countries"): Promise;
autocomplete(value: string, attribute: "originalAuthors"): Promise;
autocomplete(value: string, attribute: "authors"): Promise;
+ autocomplete(value: string, attribute: "publishers"): Promise<[]>;
autocomplete(value: string, attribute: string): Promise<[]>;
define(attribute: PublicationKey): Record;
@@ -839,6 +841,8 @@ const Publication: PublicationModule = {
case "authors":
case "originalAuthors":
return Author.REMOTE.search(value);
+ case "publishers":
+ return Publisher.REMOTE.search(value);
case "countries": {
const all = Object.values(COUNTRIES);
diff --git a/frontend/modules/publisher.ts b/frontend/modules/publisher.ts
new file mode 100644
index 00000000..2435f124
--- /dev/null
+++ b/frontend/modules/publisher.ts
@@ -0,0 +1,25 @@
+import { request } from "app";
+
+type Publisher = string;
+
+interface PublisherModule {
+ REMOTE: {
+ search(term: string): Promise;
+ };
+}
+
+const Publisher: PublisherModule = {
+ REMOTE: {
+ search(term) {
+ return request(async (http) => {
+ const { data } = await http.get("/publishers", {
+ params: { search: term },
+ });
+
+ return data;
+ });
+ },
+ },
+};
+
+export { Publisher };
From ca3ac56487180f1926f1674cede0666f2f7e02bf Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9s=20Vidal?=
Date: Tue, 25 Jun 2024 20:55:38 -0300
Subject: [PATCH 6/8] Tweak labels
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Signed-off-by: Andrés Vidal
---
frontend/modules/publication.ts | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/frontend/modules/publication.ts b/frontend/modules/publication.ts
index c4dfa2f2..9904cc08 100644
--- a/frontend/modules/publication.ts
+++ b/frontend/modules/publication.ts
@@ -392,8 +392,8 @@ const Publication: PublicationModule = {
authors: "Translators",
originalAuthors: "Original Authors",
originalTitle: "Original Title",
- countries: "Country",
- publishers: "Publisher",
+ countries: "Countries",
+ publishers: "Publishers",
title: "Title",
year: "Year",
},
From fb4de2ca941cd81e59aea9ece5ac169345fce330 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9s=20Vidal?=
Date: Tue, 25 Jun 2024 21:21:53 -0300
Subject: [PATCH 7/8] Refactor POST publications/bulk tests
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Signed-off-by: Andrés Vidal
---
.../lib/richard_burton/flat_publication.ex | 4 +
.../publication_controller_test.exs | 309 +++++++++++++++---
2 files changed, 262 insertions(+), 51 deletions(-)
diff --git a/backend/lib/richard_burton/flat_publication.ex b/backend/lib/richard_burton/flat_publication.ex
index 07cd2041..fdbaaaec 100644
--- a/backend/lib/richard_burton/flat_publication.ex
+++ b/backend/lib/richard_burton/flat_publication.ex
@@ -49,6 +49,10 @@ defmodule RichardBurton.FlatPublication do
|> TranslatedBook.link_fingerprint()
end
+ def all() do
+ Repo.all(FlatPublication)
+ end
+
def validate(attrs) do
%FlatPublication{} |> changeset(attrs) |> validate_changeset()
end
diff --git a/backend/test/richard_burton_web/controllers/publication_controller_test.exs b/backend/test/richard_burton_web/controllers/publication_controller_test.exs
index d53b1e22..5061986c 100644
--- a/backend/test/richard_burton_web/controllers/publication_controller_test.exs
+++ b/backend/test/richard_burton_web/controllers/publication_controller_test.exs
@@ -2,10 +2,13 @@ defmodule RichardBurtonWeb.PublicationControllerTest do
@moduledoc """
Tests for the Publication controller
"""
+ alias RichardBurton.FlatPublication
use RichardBurtonWeb.ConnCase
import Routes, only: [publication_path: 2]
+ alias RichardBurton.Country
alias RichardBurton.Publication
+ alias RichardBurton.Publisher
@publication_attrs %{
"title" => "Iraçéma the Honey-Lips: A Legend of Brazil",
@@ -33,78 +36,282 @@ defmodule RichardBurtonWeb.PublicationControllerTest do
end
describe "POST /publications/bulk" do
- @valid_input_1 %{
- "title" => "Manuel de Moraes: A Chronicle of the Seventeenth Century",
- "countries" => "GB",
- "year" => 1886,
- "publishers" => "Bickers & Son",
- "authors" => "Richard Burton, Isabel Burton",
- "original_authors" => "J. M. Pereira da Silva",
- "original_title" => "Manuel de Moraes: crônica do século XVII"
- }
+ test "returns 201 and the created publications when all the publications are valid", meta do
+ expect_auth_authorize_admin()
- @valid_input_2 %{
- "title" => "Iraçéma the Honey-Lips: A Legend of Brazil",
- "year" => 1886,
- "countries" => "GB",
- "publishers" => "Bickers & Son",
- "authors" => "Isabel Burton",
- "original_authors" => "José de Alencar",
- "original_title" => "Iracema"
- }
+ publications = [
+ %{
+ "title" => "Manuel de Moraes: A Chronicle of the Seventeenth Century",
+ "countries" => "GB",
+ "year" => 1886,
+ "publishers" => "Bickers & Son",
+ "authors" => "Richard Burton, Isabel Burton",
+ "original_authors" => "J. M. Pereira da Silva",
+ "original_title" => "Manuel de Moraes: crônica do século XVII"
+ },
+ %{
+ "title" => "Iraçéma the Honey-Lips: A Legend of Brazil",
+ "year" => 1886,
+ "countries" => "GB",
+ "publishers" => "Bickers & Son",
+ "authors" => "Isabel Burton",
+ "original_authors" => "José de Alencar",
+ "original_title" => "Iracema"
+ }
+ ]
- @invalid_input %{
- "title" => "",
- "year" => "1886",
- "countries" => "GB",
- "publishers" => "Bickers & Son",
- "authors" => "",
- "original_authors" => "José de Alencar",
- "original_title" => "Iracema"
- }
+ input = %{"_json" => publications}
- @invalid_input_errors %{
- "title" => "required",
- "authors" => "required"
- }
+ result =
+ meta.conn
+ |> post(publication_path(meta.conn, :create_all), input)
+ |> json_response(201)
+
+ assert publications == result
+ end
- test "on success, returns 201 and the created publications", meta do
+ test "returns 201 and inserts publications with several countries", meta do
expect_auth_authorize_admin()
- publications = [@valid_input_1, @valid_input_2]
+
+ publications = [
+ %{
+ "title" => "Manuel de Moraes: A Chronicle of the Seventeenth Century",
+ "countries" => "GB, US",
+ "year" => 1886,
+ "publishers" => "Bickers & Son",
+ "authors" => "Richard Burton, Isabel Burton",
+ "original_authors" => "J. M. Pereira da Silva",
+ "original_title" => "Manuel de Moraes: crônica do século XVII"
+ },
+ %{
+ "title" => "Iraçéma the Honey-Lips: A Legend of Brazil",
+ "year" => 1886,
+ "countries" => "GB,US",
+ "publishers" => "Bickers & Son",
+ "authors" => "Isabel Burton",
+ "original_authors" => "José de Alencar",
+ "original_title" => "Iracema"
+ },
+ %{
+ "authors" => "Isabel Burton, Richard Burton",
+ "countries" => "GB, BR,US",
+ "original_authors" => "José de Alencar",
+ "original_title" => "Iracema",
+ "publishers" => "Bickers & Son",
+ "title" => "Iraçéma the Honey-Lips: A Legend of Brazil",
+ "year" => "1886"
+ }
+ ]
input = %{"_json" => publications}
- assert publications ==
- meta.conn
- |> post(publication_path(meta.conn, :create_all), input)
- |> json_response(201)
+ result =
+ meta.conn
+ |> post(publication_path(meta.conn, :create_all), input)
+ |> json_response(201)
+
+ output = [
+ %{
+ "title" => "Manuel de Moraes: A Chronicle of the Seventeenth Century",
+ "countries" => "GB, US",
+ "year" => 1886,
+ "publishers" => "Bickers & Son",
+ "authors" => "Richard Burton, Isabel Burton",
+ "original_authors" => "J. M. Pereira da Silva",
+ "original_title" => "Manuel de Moraes: crônica do século XVII"
+ },
+ %{
+ "title" => "Iraçéma the Honey-Lips: A Legend of Brazil",
+ "year" => 1886,
+ "countries" => "GB, US",
+ "publishers" => "Bickers & Son",
+ "authors" => "Isabel Burton",
+ "original_authors" => "José de Alencar",
+ "original_title" => "Iracema"
+ },
+ %{
+ "authors" => "Isabel Burton, Richard Burton",
+ "countries" => "GB, BR, US",
+ "original_authors" => "José de Alencar",
+ "original_title" => "Iracema",
+ "publishers" => "Bickers & Son",
+ "title" => "Iraçéma the Honey-Lips: A Legend of Brazil",
+ "year" => 1886
+ }
+ ]
+
+ assert 3 == FlatPublication.all() |> length()
+ assert ["GB", "US", "BR"] == Country.all() |> Enum.map(&Country.get_code/1)
+ assert output == result
+ end
+
+ test "returns 201 and inserts publications with several publishers", meta do
+ expect_auth_authorize_admin()
+
+ publications = [
+ %{
+ "title" => "Manuel de Moraes: A Chronicle of the Seventeenth Century",
+ "countries" => "GB",
+ "year" => 1886,
+ "publishers" => "Bickers & Son,Noonday Press",
+ "authors" => "Richard Burton, Isabel Burton",
+ "original_authors" => "J. M. Pereira da Silva",
+ "original_title" => "Manuel de Moraes: crônica do século XVII"
+ },
+ %{
+ "title" => "Iraçéma the Honey-Lips: A Legend of Brazil",
+ "year" => 1886,
+ "countries" => "GB",
+ "publishers" => "Bickers & Son, Noonday Press",
+ "authors" => "Isabel Burton",
+ "original_authors" => "José de Alencar",
+ "original_title" => "Iracema"
+ },
+ %{
+ "authors" => "Isabel Burton, Richard Burton",
+ "countries" => "GB",
+ "original_authors" => "José de Alencar",
+ "original_title" => "Iracema",
+ "publishers" => "Bickers & Son, Noonday Press,Ronald Massey",
+ "title" => "Iraçéma the Honey-Lips: A Legend of Brazil",
+ "year" => "1886"
+ }
+ ]
+
+ input = %{"_json" => publications}
+
+ result =
+ meta.conn
+ |> post(publication_path(meta.conn, :create_all), input)
+ |> json_response(201)
+
+ output = [
+ %{
+ "title" => "Manuel de Moraes: A Chronicle of the Seventeenth Century",
+ "countries" => "GB",
+ "year" => 1886,
+ "publishers" => "Bickers & Son, Noonday Press",
+ "authors" => "Richard Burton, Isabel Burton",
+ "original_authors" => "J. M. Pereira da Silva",
+ "original_title" => "Manuel de Moraes: crônica do século XVII"
+ },
+ %{
+ "title" => "Iraçéma the Honey-Lips: A Legend of Brazil",
+ "year" => 1886,
+ "countries" => "GB",
+ "publishers" => "Bickers & Son, Noonday Press",
+ "authors" => "Isabel Burton",
+ "original_authors" => "José de Alencar",
+ "original_title" => "Iracema"
+ },
+ %{
+ "authors" => "Isabel Burton, Richard Burton",
+ "countries" => "GB",
+ "original_authors" => "José de Alencar",
+ "original_title" => "Iracema",
+ "publishers" => "Bickers & Son, Noonday Press, Ronald Massey",
+ "title" => "Iraçéma the Honey-Lips: A Legend of Brazil",
+ "year" => 1886
+ }
+ ]
+
+ publishers = ["Bickers & Son", "Noonday Press", "Ronald Massey"]
+
+ assert 3 == FlatPublication.all() |> length()
+ assert publishers == Publisher.all() |> Enum.map(&Publisher.get_name/1)
+ assert output == result
end
- test "on conflict, returns 409 and the conflictive publication", meta do
+ test "returns 409 when publications are repeated, and returns the first repeated one", meta do
expect_auth_authorize_admin()
- publications = [@valid_input_1, @valid_input_2, @valid_input_2]
+
+ repeated_publication = %{
+ "title" => "Iraçéma the Honey-Lips: A Legend of Brazil",
+ "year" => 1886,
+ "countries" => "GB",
+ "publishers" => "Bickers & Son",
+ "authors" => "Isabel Burton",
+ "original_authors" => "José de Alencar",
+ "original_title" => "Iracema"
+ }
+
+ publications = [
+ %{
+ "title" => "Manuel de Moraes: A Chronicle of the Seventeenth Century",
+ "countries" => "GB",
+ "year" => 1886,
+ "publishers" => "Bickers & Son",
+ "authors" => "Richard Burton, Isabel Burton",
+ "original_authors" => "J. M. Pereira da Silva",
+ "original_title" => "Manuel de Moraes: crônica do século XVII"
+ },
+ repeated_publication,
+ repeated_publication
+ ]
input = %{"_json" => publications}
- assert @valid_input_2 ==
- meta.conn
- |> post(publication_path(meta.conn, :create_all), input)
- |> json_response(409)
+ result =
+ meta.conn
+ |> post(publication_path(meta.conn, :create_all), input)
+ |> json_response(409)
+
+ assert repeated_publication == result
end
- test "on validation error, returns 409, the invalid publication and the errors", meta do
+ test "returns 400 when a publication is invalid, and returns it with its errors", meta do
expect_auth_authorize_admin()
- input = %{"_json" => [@valid_input_1, @invalid_input, @valid_input_2]}
+
+ invalid_publication = %{
+ "title" => "",
+ "year" => 1886,
+ "countries" => "GB",
+ "publishers" => "Bickers & Son",
+ "authors" => "",
+ "original_authors" => "José de Alencar",
+ "original_title" => "Iracema"
+ }
+
+ invalid_publication_errors = %{
+ "title" => "required",
+ "authors" => "required"
+ }
+
+ publications = [
+ %{
+ "title" => "Manuel de Moraes: A Chronicle of the Seventeenth Century",
+ "countries" => "GB",
+ "year" => 1886,
+ "publishers" => "Bickers & Son",
+ "authors" => "Richard Burton, Isabel Burton",
+ "original_authors" => "J. M. Pereira da Silva",
+ "original_title" => "Manuel de Moraes: crônica do século XVII"
+ },
+ invalid_publication,
+ %{
+ "title" => "Iraçéma the Honey-Lips: A Legend of Brazil",
+ "year" => 1886,
+ "countries" => "GB",
+ "publishers" => "Bickers & Son",
+ "authors" => "Isabel Burton",
+ "original_authors" => "José de Alencar",
+ "original_title" => "Iracema"
+ }
+ ]
+
+ input = %{"_json" => publications}
output = %{
- "publication" => @invalid_input,
- "errors" => @invalid_input_errors
+ "publication" => invalid_publication,
+ "errors" => invalid_publication_errors
}
- assert output ==
- meta.conn
- |> post(publication_path(meta.conn, :create_all), input)
- |> json_response(400)
+ result =
+ meta.conn
+ |> post(publication_path(meta.conn, :create_all), input)
+ |> json_response(400)
+
+ assert output == result
end
end
From 32d4612d96c09448df4afde020656b99bae81177 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9s=20Vidal?=
Date: Tue, 25 Jun 2024 21:29:26 -0300
Subject: [PATCH 8/8] Tweak PublicationModal
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Signed-off-by: Andrés Vidal
---
frontend/components/PublicationModal.tsx | 58 +++++++++++-------------
1 file changed, 27 insertions(+), 31 deletions(-)
diff --git a/frontend/components/PublicationModal.tsx b/frontend/components/PublicationModal.tsx
index a0924eba..29935920 100644
--- a/frontend/components/PublicationModal.tsx
+++ b/frontend/components/PublicationModal.tsx
@@ -1,4 +1,4 @@
-import { Publication } from "modules/publication";
+import { Publication, PublicationKey } from "modules/publication";
import Link from "next/link";
import { FC } from "react";
import { z } from "zod";
@@ -33,7 +33,7 @@ const SearchableList: FC<{ items: { label: string; value?: string }[] }> = ({
{items.map((item, index) => (
-
- {index === items.length - 1 && " and "}
+ {index != 0 && index === items.length - 1 && " and "}
{index < items.length - 2 && ", "}
{index === items.length - 1 && " "}
@@ -60,35 +60,31 @@ const PublicationHeading: FC<{ publication: Publication }> = ({
);
const PublicationDescription: FC<{ publication: Publication }> = ({
- publication,
-}) => (
-
- is a translation of{" "}
- , by{" "}
- . It was written by{" "}
- and published in{" "}
- id.trim())
- .map((id) => ({
- id,
- label: Publication.describeValue(id, "countries"),
- }))}
- />
- in {publication?.year} by{" "}
- id.trim())
- .map((id) => ({
- id,
- label: Publication.describeValue(id, "publishers"),
- }))}
- />
- .
-
-);
+ publication: p,
+}) => {
+ function getSearchableItems(p: Publication, key: PublicationKey) {
+ return p[key]
+ .split(",")
+ .map((id) => id.trim())
+ .map((id) => ({
+ id,
+ label: Publication.describeValue(id, key),
+ }));
+ }
+
+ return (
+
+ is a translation of{" "}
+ , by{" "}
+ . It
+ was written by {" "}
+ and published in{" "}
+
+ in {p.year} by{" "}
+ .
+
+ );
+};
const PublicationModal: FC = () => {
const { value, ...modal } = useURLQueryModal(PUBLICATION_MODAL_KEY);