120 lines
3.9 KiB
Elixir
120 lines
3.9 KiB
Elixir
defmodule MvWeb.ImportTemplateController do
|
|
@moduledoc """
|
|
Serves CSV import templates generated on the fly from the current custom fields.
|
|
|
|
Two actions provide an English (`en/2`) and a German (`de/2`) template. Each
|
|
template has a single header row listing the standard member columns followed
|
|
by every existing custom field name (exact match, as the import expects), plus
|
|
the importable groups and fee-type columns. A single placeholder example row is
|
|
included to illustrate the format.
|
|
|
|
Both actions require the same authorization as the import page
|
|
(`can?(:create, Member)`); unauthorized requests are rejected.
|
|
"""
|
|
use MvWeb, :controller
|
|
|
|
alias Mv.Authorization.Actor
|
|
alias Mv.Membership.Member
|
|
alias Mv.Membership.MembersCSV
|
|
alias MvWeb.Authorization
|
|
|
|
# Standard member columns in template order, with their English and German headers
|
|
# and a placeholder example value. Groups and fee type are importable extras.
|
|
@columns [
|
|
{"first name", "Vorname", "John", "Max"},
|
|
{"last name", "Nachname", "Doe", "Mustermann"},
|
|
{"email", "E-Mail", "john.doe@example.com", "max.mustermann@example.com"},
|
|
{"country", "Land", "Germany", "Deutschland"},
|
|
{"city", "Stadt", "Berlin", "Berlin"},
|
|
{"street", "Straße", "Main Street", "Hauptstraße"},
|
|
{"house number", "Hausnummer", "1a", "12"},
|
|
{"postal_code", "PLZ", "12345", "10115"},
|
|
{"join_date", "Beitrittsdatum", "2020-01-15", "2020-01-15"},
|
|
{"exit_date", "Austrittsdatum", "", ""},
|
|
{"notes", "Notizen", "", ""},
|
|
{"membership_fee_start_date", "Beitragsbeginn", "", ""},
|
|
{"Groups", "Gruppen", "", ""},
|
|
{"Fee Type", "Beitragsart", "", ""}
|
|
]
|
|
|
|
@spec en(Plug.Conn.t(), map()) :: Plug.Conn.t()
|
|
def en(conn, _params) do
|
|
serve_template(conn, :en, "member_import_en.csv")
|
|
end
|
|
|
|
@spec de(Plug.Conn.t(), map()) :: Plug.Conn.t()
|
|
def de(conn, _params) do
|
|
serve_template(conn, :de, "member_import_de.csv")
|
|
end
|
|
|
|
defp serve_template(conn, locale, filename) do
|
|
actor = current_actor(conn)
|
|
|
|
if Authorization.can?(actor, :create, Member) do
|
|
csv = build_csv(locale, actor)
|
|
|
|
send_download(conn, {:binary, csv},
|
|
filename: filename,
|
|
content_type: "text/csv; charset=utf-8"
|
|
)
|
|
else
|
|
return_forbidden(conn)
|
|
end
|
|
end
|
|
|
|
defp build_csv(locale, actor) do
|
|
custom_field_names = custom_field_names(actor)
|
|
|
|
header =
|
|
Enum.map(@columns, &header_for(&1, locale)) ++ custom_field_names
|
|
|
|
example =
|
|
Enum.map(@columns, &example_for(&1, locale)) ++ Enum.map(custom_field_names, fn _ -> "" end)
|
|
|
|
[csv_row(header), csv_row(example)]
|
|
|> Enum.join("\n")
|
|
end
|
|
|
|
defp header_for({en, _de, _ex_en, _ex_de}, :en), do: en
|
|
defp header_for({_en, de, _ex_en, _ex_de}, :de), do: de
|
|
|
|
defp example_for({_en, _de, ex_en, _ex_de}, :en), do: ex_en
|
|
defp example_for({_en, _de, _ex_en, ex_de}, :de), do: ex_de
|
|
|
|
defp custom_field_names(actor) do
|
|
Mv.Membership.list_custom_fields!(actor: actor)
|
|
|> Enum.map(& &1.name)
|
|
end
|
|
|
|
# Serializes a row using the semicolon delimiter (the import auto-detects it),
|
|
# quoting any field that contains a delimiter, quote, or newline.
|
|
defp csv_row(fields) do
|
|
Enum.map_join(fields, ";", &escape_field/1)
|
|
end
|
|
|
|
# Neutralizes spreadsheet formula triggers (the same guard the export writer
|
|
# applies) before RFC 4180 quoting, so a custom-field name like
|
|
# `=HYPERLINK(...)` is not evaluated when the template is opened.
|
|
defp escape_field(field) do
|
|
field = field |> to_string() |> MembersCSV.safe_cell()
|
|
|
|
if String.contains?(field, [";", "\"", "\n", "\r"]) do
|
|
"\"" <> String.replace(field, "\"", "\"\"") <> "\""
|
|
else
|
|
field
|
|
end
|
|
end
|
|
|
|
defp current_actor(conn) do
|
|
conn.assigns[:current_user]
|
|
|> Actor.ensure_loaded()
|
|
end
|
|
|
|
defp return_forbidden(conn) do
|
|
conn
|
|
|> put_status(403)
|
|
|> put_resp_content_type("application/json")
|
|
|> json(%{error: "Forbidden"})
|
|
|> halt()
|
|
end
|
|
end
|