Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 90 additions & 0 deletions lib/voting/voter/import_voters.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
defmodule Voting.ImportVoters do
@moduledoc """
Importing voters from a CSV file
"""

alias Voting.{ElectionRepo, Repo, Voter}

import Ecto.Changeset

def run(file, election_id) do
election = ElectionRepo.get_election!(election_id)
now = NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second)

rows =
file
|> File.stream!()
|> CSV.decode(headers: true)
|> Stream.map(fn row ->
case row do
{:ok, voter} -> {:ok, build_entry(voter, election.id, now)}
{:error, error} -> {:error, error}
end
end)
|> Enum.to_list()

entries =
rows
|> Keyword.get_values(:ok)
|> validate_entries()
|> find_voters()
|> insert_entries()

%{
inserted: entries.inserted,
invalid: entries.invalid,
already_exist: entries.already_exist,
errors: Keyword.get_values(rows, :error)
}
end

defp build_entry(voter, election_id, now) do
%{
name: Map.get(voter, "name"),
registration_number: Map.get(voter, "registration_number"),
role: Map.get(voter, "role"),
admission_date: parse_date(Map.get(voter, "admission_date")),
inserted_at: now,
updated_at: now,
election_id: election_id
}
end

defp parse_date(value) do
case Timex.parse(value, "{D}/{0M}/{YYYY}") do
{:ok, date} -> Timex.to_date(date)
{:error, _} -> ""
end
end

defp validate_entries(entries) do
{valid, invalid} =
Enum.split_with(entries, fn entry ->
%Voter{}
|> cast(entry, [:name, :registration_number, :role, :admission_date])
|> validate_required([:name, :registration_number, :role, :admission_date])
|> Map.get(:valid?)
end)

%{valid: valid, invalid: invalid}
end

defp find_voters(%{valid: entries, invalid: invalid}) do
{insertable, already_exist} =
Enum.split_with(entries, fn entry ->
Voter
|> Repo.get_by(%{
election_id: entry.election_id,
registration_number: entry.registration_number
})
|> is_nil()
end)

%{valid: insertable, invalid: invalid, already_exist: already_exist}
end

defp insert_entries(%{valid: entries, invalid: invalid, already_exist: already_exist}) do
Repo.insert_all(Voter, entries)
%{inserted: entries, invalid: invalid, already_exist: already_exist}
end
end
20 changes: 20 additions & 0 deletions lib/voting/voter/voter.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
defmodule Voting.Voter do
@moduledoc """
Voter schema
"""

use Ecto.Schema

alias Voting.Election

schema "voters" do
field :admission_date, :date
field :name, :string
field :registration_number, :string
field :role, :string
field :voted, :boolean, default: false
belongs_to :election, Election

timestamps()
end
end
6 changes: 4 additions & 2 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ defmodule Voting.MixProject do
def application do
[
mod: {Voting.Application, []},
extra_applications: [:logger, :runtime_tools]
extra_applications: [:logger, :runtime_tools, :timex]
]
end

Expand Down Expand Up @@ -50,7 +50,9 @@ defmodule Voting.MixProject do
{:hackney, "~> 1.9"},
{:sweet_xml, "~> 0.6"},
{:uuid, "~> 1.1"},
{:mimic, "~> 1.2", only: :test}
{:mimic, "~> 1.2", only: :test},
{:csv, "~> 2.3"},
{:timex, "~> 3.5"}
]
end

Expand Down
5 changes: 5 additions & 0 deletions mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@
"bcrypt_elixir": {:hex, :bcrypt_elixir, "2.2.0", "3df902b81ce7fa8867a2ae30d20a1da6877a2c056bfb116fd0bc8a5f0190cea4", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "762be3fcb779f08207531bc6612cca480a338e4b4357abb49f5ce00240a77d1e"},
"bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"},
"certifi": {:hex, :certifi, "2.5.1", "867ce347f7c7d78563450a18a6a28a8090331e77fa02380b4a21962a65d36ee5", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm", "805abd97539caf89ec6d4732c91e62ba9da0cda51ac462380bbd28ee697a8c42"},
"combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"},
"comeonin": {:hex, :comeonin, "5.3.1", "7fe612b739c78c9c1a75186ef2d322ce4d25032d119823269d0aa1e2f1e20025", [:mix], [], "hexpm", "d6222483060c17f0977fad1b7401ef0c5863c985a64352755f366aee3799c245"},
"connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm", "4a0850c9be22a43af9920a71ab17c051f5f7d45c209e40269a1938832510e4d9"},
"cowboy": {:hex, :cowboy, "2.7.0", "91ed100138a764355f43316b1d23d7ff6bdb0de4ea618cb5d8677c93a7a2f115", [:rebar3], [{:cowlib, "~> 2.8.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.1", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "04fd8c6a39edc6aaa9c26123009200fc61f92a3a94f3178c527b70b767c6e605"},
"cowlib": {:hex, :cowlib, "2.8.0", "fd0ff1787db84ac415b8211573e9a30a3ebe71b5cbff7f720089972b2319c8a4", [:rebar3], [], "hexpm", "79f954a7021b302186a950a32869dbc185523d99d3e44ce430cd1f3289f41ed4"},
"credo": {:hex, :credo, "1.3.2", "08d456dcf3c24da162d02953fb07267e444469d8dad3a2ae47794938ea467b3a", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "b11d28cce1f1f399dddffd42d8e21dcad783309e230f84b70267b1a5546468b6"},
"csv": {:hex, :csv, "2.3.1", "9ce11eff5a74a07baf3787b2b19dd798724d29a9c3a492a41df39f6af686da0e", [:mix], [{:parallel_stream, "~> 1.0.4", [hex: :parallel_stream, repo: "hexpm", optional: false]}], "hexpm", "86626e1c89a4ad9a96d0d9c638f9e88c2346b89b4ba1611988594ebe72b5d5ee"},
"db_connection": {:hex, :db_connection, "2.2.1", "caee17725495f5129cb7faebde001dc4406796f12a62b8949f4ac69315080566", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}], "hexpm", "2b02ece62d9f983fcd40954e443b7d9e6589664380e5546b2b9b523cd0fb59e1"},
"decimal": {:hex, :decimal, "1.8.1", "a4ef3f5f3428bdbc0d35374029ffcf4ede8533536fa79896dd450168d9acdf3c", [:mix], [], "hexpm", "3cb154b00225ac687f6cbd4acc4b7960027c757a5152b369923ead9ddbca7aec"},
"ecto": {:hex, :ecto, "3.4.0", "a7a83ab8359bf816ce729e5e65981ce25b9fc5adfc89c2ea3980f4fed0bfd7c1", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "5eed18252f5b5bbadec56a24112b531343507dbe046273133176b12190ce19cc"},
Expand All @@ -25,6 +27,7 @@
"mime": {:hex, :mime, "1.3.1", "30ce04ab3175b6ad0bdce0035cba77bba68b813d523d1aac73d9781b4d193cf8", [:mix], [], "hexpm", "6cbe761d6a0ca5a31a0931bf4c63204bceb64538e664a8ecf784a9a6f3b875f1"},
"mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"},
"mimic": {:hex, :mimic, "1.2.0", "08f783232033bc69c1c1264e4a1cceb8de6f07caea9eab3440290437743a7593", [:mix], [], "hexpm", "fed5a64c49e544e60caa9b60af1f97eb40447ed9bd5857f992c2fffd3ac9a6de"},
"parallel_stream": {:hex, :parallel_stream, "1.0.6", "b967be2b23f0f6787fab7ed681b4c45a215a81481fb62b01a5b750fa8f30f76c", [:mix], [], "hexpm", "639b2e8749e11b87b9eb42f2ad325d161c170b39b288ac8d04c4f31f8f0823eb"},
"parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm", "17ef63abde837ad30680ea7f857dd9e7ced9476cdd7b0394432af4bfc241b960"},
"phoenix": {:hex, :phoenix, "1.4.16", "2cbbe0c81e6601567c44cc380c33aa42a1372ac1426e3de3d93ac448a7ec4308", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.8.1 or ~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "856cc1a032fa53822737413cf51aa60e750525d7ece7d1c0576d90d7c0f05c24"},
"phoenix_ecto": {:hex, :phoenix_ecto, "4.1.0", "a044d0756d0464c5a541b4a0bf4bcaf89bffcaf92468862408290682c73ae50d", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.9", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "c5e666a341ff104d0399d8f0e4ff094559b2fde13a5985d4cb5023b2c2ac558b"},
Expand All @@ -37,6 +40,8 @@
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.5", "6eaf7ad16cb568bb01753dbbd7a95ff8b91c7979482b95f38443fe2c8852a79b", [:make, :mix, :rebar3], [], "hexpm", "13104d7897e38ed7f044c4de953a6c28597d1c952075eb2e328bc6d6f2bfc496"},
"sweet_xml": {:hex, :sweet_xml, "0.6.6", "fc3e91ec5dd7c787b6195757fbcf0abc670cee1e4172687b45183032221b66b8", [:mix], [], "hexpm", "2e1ec458f892ffa81f9f8386e3f35a1af6db7a7a37748a64478f13163a1f3573"},
"telemetry": {:hex, :telemetry, "0.4.1", "ae2718484892448a24470e6aa341bc847c3277bfb8d4e9289f7474d752c09c7f", [:rebar3], [], "hexpm", "4738382e36a0a9a2b6e25d67c960e40e1a2c95560b9f936d8e29de8cd858480f"},
"timex": {:hex, :timex, "3.6.1", "efdf56d0e67a6b956cc57774353b0329c8ab7726766a11547e529357ffdc1d56", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5 or ~> 1.0.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "f354efb2400dd7a80fd9eb6c8419068c4f632da4ac47f3d8822d6e33f08bc852"},
"tzdata": {:hex, :tzdata, "1.0.3", "73470ad29dde46e350c60a66e6b360d3b99d2d18b74c4c349dbebbc27a09a3eb", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "a6e1ee7003c4d04ecbd21dd3ec690d4c6662db5d3bbdd7262d53cdf5e7c746c1"},
"unicode_util_compat": {:hex, :unicode_util_compat, "0.4.1", "d869e4c68901dd9531385bb0c8c40444ebf624e60b6962d95952775cac5e90cd", [:rebar3], [], "hexpm", "1d1848c40487cdb0b30e8ed975e34e025860c02e419cb615d255849f3427439d"},
"uuid": {:hex, :uuid, "1.1.8", "e22fc04499de0de3ed1116b770c7737779f226ceefa0badb3592e64d5cfb4eb9", [:mix], [], "hexpm", "c790593b4c3b601f5dc2378baae7efaf5b3d73c4c6456ba85759905be792f2ac"},
}
19 changes: 19 additions & 0 deletions priv/repo/migrations/20200404175521_create_voters.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
defmodule Voting.Repo.Migrations.CreateVoters do
use Ecto.Migration

def change do
create table(:voters) do
add :name, :string, null: false
add :admission_date, :date, null: false
add :registration_number, :string, null: false
add :role, :string, null: false
add :voted, :boolean, default: false, null: false
add :election_id, references(:elections, on_delete: :nothing)

timestamps()
end

create index(:voters, [:election_id])
create unique_index(:voters, [:election_id, :registration_number])
end
end
6 changes: 6 additions & 0 deletions test/csv/voters.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
name,admission_date,registration_number,role
Thiago Guimarães,10/01/2019,1,Programador
Maria dos Santos,10/01/2019,2,Secretária
José dos Santos;10/01/2019;3;Segurança
João dos Santos,10/01/2019,4,Porteiro
Roberto dos Santos,,5,Contador
56 changes: 56 additions & 0 deletions test/voting/voter/import_voters_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
defmodule Voting.ImportVotersTest do
use Voting.DataCase, async: true

import Voting.Factory

alias Voting.ImportVoters

describe "run/2" do
test "returns the inserted, invalid, already_exist and errors" do
%{id: election_id} = election = insert(:election)
csv = Path.expand("../../csv/voters.csv", __DIR__)

assert %{
inserted: inserted,
invalid: invalid,
already_exist: already_exist,
errors: errors
} = ImportVoters.run(csv, election.id)

assert [
%{registration_number: "1", election_id: ^election_id},
%{registration_number: "2", election_id: ^election_id},
%{registration_number: "4", election_id: ^election_id}
] = inserted

assert [
%{registration_number: "5", election_id: ^election_id}
] = invalid

assert [] = already_exist

assert ["Row has length 1 - expected length 4 on line 4"] = errors

assert %{
inserted: inserted,
invalid: invalid,
already_exist: already_exist,
errors: errors
} = ImportVoters.run(csv, election.id)

assert [] = inserted

assert [
%{registration_number: "1", election_id: ^election_id},
%{registration_number: "2", election_id: ^election_id},
%{registration_number: "4", election_id: ^election_id}
] = already_exist

assert [
%{registration_number: "5", election_id: ^election_id}
] = invalid

assert ["Row has length 1 - expected length 4 on line 4"] = errors
end
end
end