feat: adds pdf export with imprintor
Some checks failed
continuous-integration/drone/push Build is failing
Some checks failed
continuous-integration/drone/push Build is failing
This commit is contained in:
parent
496e2e438f
commit
f6b35f03a5
16 changed files with 1962 additions and 70 deletions
144
lib/mv_web/controllers/member_pdf_export_controller.ex
Normal file
144
lib/mv_web/controllers/member_pdf_export_controller.ex
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
defmodule MvWeb.MemberPdfExportController do
|
||||
@moduledoc """
|
||||
PDF export for members.
|
||||
|
||||
Expects `payload` as JSON string form param.
|
||||
Uses the same actor/permissions as the member overview.
|
||||
"""
|
||||
|
||||
use MvWeb, :controller
|
||||
|
||||
require Logger
|
||||
|
||||
alias Mv.Authorization.Actor
|
||||
alias Mv.Membership.{MemberExport, MemberExport.Build, MembersPDF}
|
||||
alias MvWeb.Translations.MemberFields
|
||||
|
||||
use Gettext, backend: MvWeb.Gettext
|
||||
|
||||
@payload_required_message "payload required"
|
||||
@invalid_json_message "invalid JSON"
|
||||
@export_failed_message "Failed to generate PDF export"
|
||||
|
||||
@allowed_member_field_strings Mv.Constants.member_fields() |> Enum.map(&Atom.to_string/1)
|
||||
|
||||
def export(conn, %{"payload" => payload}) when is_binary(payload) do
|
||||
actor = current_actor(conn)
|
||||
|
||||
cond do
|
||||
is_nil(actor) ->
|
||||
forbidden(conn)
|
||||
|
||||
true ->
|
||||
with {:ok, decoded} <- decode_json_map(payload),
|
||||
parsed <- MemberExport.parse_params(decoded),
|
||||
{:ok, export_data} <- Build.build(actor, parsed, &label_for_column/1),
|
||||
{:ok, pdf_binary} <- MembersPDF.render(export_data) do
|
||||
filename = "members-#{Date.utc_today()}.pdf"
|
||||
|
||||
send_download(
|
||||
conn,
|
||||
{:binary, pdf_binary},
|
||||
filename: filename,
|
||||
content_type: "application/pdf"
|
||||
)
|
||||
else
|
||||
{:error, :invalid_json} ->
|
||||
bad_request(conn, @invalid_json_message)
|
||||
|
||||
{:error, :forbidden} ->
|
||||
forbidden(conn)
|
||||
|
||||
{:error, {:row_limit_exceeded, row_count, max_rows}} ->
|
||||
unprocessable_entity(conn, %{
|
||||
error: "row_limit_exceeded",
|
||||
message:
|
||||
gettext("Export contains %{count} rows, maximum is %{max}",
|
||||
count: row_count,
|
||||
max: max_rows
|
||||
),
|
||||
row_count: row_count,
|
||||
max_rows: max_rows
|
||||
})
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.warning("PDF export failed: #{inspect(reason)}")
|
||||
|
||||
internal_error(conn, %{
|
||||
error: "export_failed",
|
||||
message: gettext(@export_failed_message)
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def export(conn, _params) do
|
||||
bad_request(conn, @payload_required_message)
|
||||
end
|
||||
|
||||
# --- Actor / auth ---
|
||||
|
||||
defp current_actor(conn) do
|
||||
conn.assigns[:current_user]
|
||||
|> Actor.ensure_loaded()
|
||||
end
|
||||
|
||||
defp forbidden(conn) do
|
||||
conn
|
||||
|> put_status(:forbidden)
|
||||
|> json(%{error: "forbidden", message: "Forbidden"})
|
||||
|> halt()
|
||||
end
|
||||
|
||||
# --- Decoding / validation ---
|
||||
|
||||
defp decode_json_map(payload) when is_binary(payload) do
|
||||
case Jason.decode(payload) do
|
||||
{:ok, decoded} when is_map(decoded) -> {:ok, decoded}
|
||||
_ -> {:error, :invalid_json}
|
||||
end
|
||||
end
|
||||
|
||||
# --- Column labels ---
|
||||
|
||||
# Goal: translate known member fields to UI labels, but never crash.
|
||||
# - Atoms: label directly.
|
||||
# - Binaries: only translate if they are known member fields (allowlist); otherwise return the string.
|
||||
# This avoids String.to_existing_atom/1 exceptions for arbitrary keys (e.g., "custom_field_...").
|
||||
defp label_for_column(key) when is_atom(key) do
|
||||
MemberFields.label(key)
|
||||
end
|
||||
|
||||
defp label_for_column(key) when is_binary(key) do
|
||||
if key in @allowed_member_field_strings do
|
||||
# Safe because key is in allowlist which originates from existing atoms
|
||||
MemberFields.label(String.to_existing_atom(key))
|
||||
else
|
||||
key
|
||||
end
|
||||
end
|
||||
|
||||
defp label_for_column(key) do
|
||||
to_string(key)
|
||||
end
|
||||
|
||||
# --- JSON responses ---
|
||||
|
||||
defp bad_request(conn, message) when is_binary(message) do
|
||||
conn
|
||||
|> put_status(:bad_request)
|
||||
|> json(%{error: "bad_request", message: message})
|
||||
end
|
||||
|
||||
defp unprocessable_entity(conn, body) when is_map(body) do
|
||||
conn
|
||||
|> put_status(:unprocessable_entity)
|
||||
|> json(body)
|
||||
end
|
||||
|
||||
defp internal_error(conn, body) when is_map(body) do
|
||||
conn
|
||||
|> put_status(:internal_server_error)
|
||||
|> json(body)
|
||||
end
|
||||
end
|
||||
Loading…
Add table
Add a link
Reference in a new issue