refactor(member-export): remove dead fetch/2 export chain
This commit is contained in:
parent
a9932776cc
commit
c4a695329c
1 changed files with 0 additions and 273 deletions
|
|
@ -7,12 +7,6 @@ defmodule Mv.Membership.MemberExport do
|
||||||
and sends the download.
|
and sends the download.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
require Ash.Query
|
|
||||||
import Ash.Expr
|
|
||||||
|
|
||||||
alias Mv.Membership.CustomField
|
|
||||||
alias Mv.Membership.Member
|
|
||||||
alias Mv.Membership.MemberExportSort
|
|
||||||
alias MvWeb.MemberLive.Index
|
alias MvWeb.MemberLive.Index
|
||||||
alias MvWeb.MemberLive.Index.MembershipFeeStatus
|
alias MvWeb.MemberLive.Index.MembershipFeeStatus
|
||||||
|
|
||||||
|
|
@ -35,261 +29,8 @@ defmodule Mv.Membership.MemberExport do
|
||||||
["membership_fee_type", "membership_fee_status", "groups"]
|
["membership_fee_type", "membership_fee_status", "groups"]
|
||||||
@computed_export_fields ["membership_fee_status"]
|
@computed_export_fields ["membership_fee_status"]
|
||||||
@computed_insert_after "membership_fee_start_date"
|
@computed_insert_after "membership_fee_start_date"
|
||||||
@custom_field_prefix Mv.Constants.custom_field_prefix()
|
|
||||||
@domain_member_field_strings Mv.Constants.member_fields() |> Enum.map(&Atom.to_string/1)
|
@domain_member_field_strings Mv.Constants.member_fields() |> Enum.map(&Atom.to_string/1)
|
||||||
|
|
||||||
@doc """
|
|
||||||
Fetches members and column specs for export.
|
|
||||||
|
|
||||||
- `actor` - Ash actor (e.g. current user)
|
|
||||||
- `parsed` - Map from controller's parse_and_validate (selected_ids, member_fields, etc.)
|
|
||||||
|
|
||||||
Returns `{:ok, members, column_specs}` or `{:error, :forbidden}`.
|
|
||||||
Column specs have `:kind`, `:key`, and for custom fields `:custom_field`;
|
|
||||||
the controller adds `:header` and optional computed columns to members before CSV export.
|
|
||||||
"""
|
|
||||||
@spec fetch(struct(), map()) ::
|
|
||||||
{:ok, [struct()], [map()]} | {:error, :forbidden}
|
|
||||||
def fetch(actor, parsed) do
|
|
||||||
custom_field_ids_union =
|
|
||||||
(parsed.custom_field_ids ++ Map.keys(parsed.boolean_filters || %{})) |> Enum.uniq()
|
|
||||||
|
|
||||||
with {:ok, custom_fields_by_id} <- load_custom_fields_by_id(custom_field_ids_union, actor),
|
|
||||||
{:ok, members} <- load_members(actor, parsed, custom_fields_by_id) do
|
|
||||||
column_specs = build_column_specs(parsed, custom_fields_by_id)
|
|
||||||
{:ok, members, column_specs}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp load_custom_fields_by_id([], _actor), do: {:ok, %{}}
|
|
||||||
|
|
||||||
defp load_custom_fields_by_id(custom_field_ids, actor) do
|
|
||||||
query =
|
|
||||||
CustomField
|
|
||||||
|> Ash.Query.filter(expr(id in ^custom_field_ids))
|
|
||||||
|> Ash.Query.select([:id, :name, :value_type])
|
|
||||||
|
|
||||||
case Ash.read(query, actor: actor) do
|
|
||||||
{:ok, custom_fields} ->
|
|
||||||
by_id = build_custom_fields_by_id(custom_field_ids, custom_fields)
|
|
||||||
{:ok, by_id}
|
|
||||||
|
|
||||||
{:error, %Ash.Error.Forbidden{}} ->
|
|
||||||
{:error, :forbidden}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp build_custom_fields_by_id(custom_field_ids, custom_fields) do
|
|
||||||
Enum.reduce(custom_field_ids, %{}, fn id, acc ->
|
|
||||||
find_and_add_custom_field(acc, id, custom_fields)
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp find_and_add_custom_field(acc, id, custom_fields) do
|
|
||||||
case Enum.find(custom_fields, fn cf -> to_string(cf.id) == to_string(id) end) do
|
|
||||||
nil -> acc
|
|
||||||
cf -> Map.put(acc, id, cf)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp build_column_specs(parsed, custom_fields_by_id) do
|
|
||||||
member_specs = build_member_column_specs(parsed)
|
|
||||||
custom_specs = build_custom_column_specs(parsed, custom_fields_by_id)
|
|
||||||
|
|
||||||
member_specs ++ custom_specs
|
|
||||||
end
|
|
||||||
|
|
||||||
defp build_member_column_specs(parsed) do
|
|
||||||
Enum.map(parsed.member_fields, fn f ->
|
|
||||||
build_single_member_spec(f, parsed.selectable_member_fields)
|
|
||||||
end)
|
|
||||||
|> Enum.reject(&is_nil/1)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp build_single_member_spec(field, selectable_member_fields) do
|
|
||||||
if field in selectable_member_fields do
|
|
||||||
%{kind: :member_field, key: field}
|
|
||||||
else
|
|
||||||
build_computed_spec(field)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp build_computed_spec(field) do
|
|
||||||
# only allow known computed export fields to avoid crashing on unknown atoms
|
|
||||||
if field in @computed_export_fields do
|
|
||||||
%{kind: :computed, key: String.to_existing_atom(field)}
|
|
||||||
else
|
|
||||||
# ignore unknown non-selectable fields defensively
|
|
||||||
nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp build_custom_column_specs(parsed, custom_fields_by_id) do
|
|
||||||
parsed.custom_field_ids
|
|
||||||
|> Enum.map(fn id -> Map.get(custom_fields_by_id, id) end)
|
|
||||||
|> Enum.reject(&is_nil/1)
|
|
||||||
|> Enum.map(fn cf -> %{kind: :custom_field, key: cf.id, custom_field: cf} end)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp load_members(actor, parsed, custom_fields_by_id) do
|
|
||||||
query = build_members_query(parsed, custom_fields_by_id)
|
|
||||||
|
|
||||||
case Ash.read(query, actor: actor) do
|
|
||||||
{:ok, members} ->
|
|
||||||
processed_members = process_loaded_members(members, parsed, custom_fields_by_id)
|
|
||||||
{:ok, processed_members}
|
|
||||||
|
|
||||||
{:error, %Ash.Error.Forbidden{}} ->
|
|
||||||
{:error, :forbidden}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp build_members_query(parsed, _custom_fields_by_id) do
|
|
||||||
select_fields =
|
|
||||||
[:id] ++ Enum.map(parsed.selectable_member_fields, &String.to_existing_atom/1)
|
|
||||||
|
|
||||||
custom_field_ids_union = parsed.custom_field_ids ++ Map.keys(parsed.boolean_filters || %{})
|
|
||||||
|
|
||||||
need_cycles =
|
|
||||||
parsed.show_current_cycle or parsed.cycle_status_filter != nil or
|
|
||||||
parsed.computed_fields != [] or
|
|
||||||
"membership_fee_status" in parsed.member_fields
|
|
||||||
|
|
||||||
query =
|
|
||||||
Member
|
|
||||||
|> Ash.Query.new()
|
|
||||||
|> Ash.Query.select(select_fields)
|
|
||||||
|> load_custom_field_values_query(custom_field_ids_union)
|
|
||||||
|> maybe_load_cycles(need_cycles, parsed.show_current_cycle)
|
|
||||||
|
|
||||||
if parsed.selected_ids != [] do
|
|
||||||
Ash.Query.filter(query, expr(id in ^parsed.selected_ids))
|
|
||||||
else
|
|
||||||
query
|
|
||||||
|> apply_search(parsed.query)
|
|
||||||
|> then(fn q ->
|
|
||||||
{q, _sort_after_load} = maybe_sort(q, parsed.sort_field, parsed.sort_order)
|
|
||||||
q
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp process_loaded_members(members, parsed, custom_fields_by_id) do
|
|
||||||
members
|
|
||||||
|> apply_post_load_filters(parsed, custom_fields_by_id)
|
|
||||||
|> apply_post_load_sorting(parsed, custom_fields_by_id)
|
|
||||||
|> add_computed_fields(parsed.computed_fields, parsed.show_current_cycle)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp apply_post_load_filters(members, parsed, custom_fields_by_id) do
|
|
||||||
if parsed.selected_ids == [] do
|
|
||||||
members
|
|
||||||
|> apply_cycle_status_filter(parsed.cycle_status_filter, parsed.show_current_cycle)
|
|
||||||
|> Index.apply_boolean_custom_field_filters(
|
|
||||||
parsed.boolean_filters || %{},
|
|
||||||
Map.values(custom_fields_by_id)
|
|
||||||
)
|
|
||||||
else
|
|
||||||
members
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp apply_post_load_sorting(members, parsed, custom_fields_by_id) do
|
|
||||||
if parsed.selected_ids == [] and sort_after_load?(parsed.sort_field) do
|
|
||||||
sort_members_by_custom_field(
|
|
||||||
members,
|
|
||||||
parsed.sort_field,
|
|
||||||
parsed.sort_order,
|
|
||||||
Map.values(custom_fields_by_id)
|
|
||||||
)
|
|
||||||
else
|
|
||||||
members
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp load_custom_field_values_query(query, []), do: query
|
|
||||||
|
|
||||||
defp load_custom_field_values_query(query, custom_field_ids) do
|
|
||||||
cfv_query =
|
|
||||||
Mv.Membership.CustomFieldValue
|
|
||||||
|> Ash.Query.filter(expr(custom_field_id in ^custom_field_ids))
|
|
||||||
|> Ash.Query.load(custom_field: [:id, :name, :value_type])
|
|
||||||
|
|
||||||
Ash.Query.load(query, custom_field_values: cfv_query)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp apply_search(query, nil), do: query
|
|
||||||
defp apply_search(query, ""), do: query
|
|
||||||
|
|
||||||
defp apply_search(query, q) when is_binary(q) do
|
|
||||||
if String.trim(q) != "" do
|
|
||||||
Member.fuzzy_search(query, %{query: q})
|
|
||||||
else
|
|
||||||
query
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp maybe_sort(query, nil, _order), do: {query, false}
|
|
||||||
defp maybe_sort(query, _field, nil), do: {query, false}
|
|
||||||
|
|
||||||
defp maybe_sort(query, field, order) when is_binary(field) do
|
|
||||||
if custom_field_sort?(field) do
|
|
||||||
{query, true}
|
|
||||||
else
|
|
||||||
field_atom = String.to_existing_atom(field)
|
|
||||||
|
|
||||||
if field_atom in (Mv.Constants.member_fields() -- [:notes]) do
|
|
||||||
{Ash.Query.sort(query, [{field_atom, String.to_existing_atom(order)}]), false}
|
|
||||||
else
|
|
||||||
{query, false}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
rescue
|
|
||||||
ArgumentError -> {query, false}
|
|
||||||
end
|
|
||||||
|
|
||||||
defp sort_after_load?(field) when is_binary(field),
|
|
||||||
do: String.starts_with?(field, @custom_field_prefix)
|
|
||||||
|
|
||||||
defp sort_after_load?(_), do: false
|
|
||||||
|
|
||||||
defp sort_members_by_custom_field(members, _field, _order, _custom_fields) when members == [],
|
|
||||||
do: []
|
|
||||||
|
|
||||||
defp sort_members_by_custom_field(members, field, order, custom_fields) when is_binary(field) do
|
|
||||||
id_str = String.trim_leading(field, @custom_field_prefix)
|
|
||||||
custom_field = Enum.find(custom_fields, fn cf -> to_string(cf.id) == id_str end)
|
|
||||||
if is_nil(custom_field), do: members
|
|
||||||
|
|
||||||
key_fn = fn member ->
|
|
||||||
cfv = find_cfv(member, custom_field)
|
|
||||||
raw = if cfv, do: cfv.value, else: nil
|
|
||||||
MemberExportSort.custom_field_sort_key(custom_field.value_type, raw)
|
|
||||||
end
|
|
||||||
|
|
||||||
members
|
|
||||||
|> Enum.map(fn m -> {m, key_fn.(m)} end)
|
|
||||||
|> Enum.sort(fn {_, ka}, {_, kb} -> MemberExportSort.key_lt(ka, kb, order) end)
|
|
||||||
|> Enum.map(fn {m, _} -> m end)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp find_cfv(member, custom_field) do
|
|
||||||
(member.custom_field_values || [])
|
|
||||||
|> Enum.find(fn cfv ->
|
|
||||||
to_string(cfv.custom_field_id) == to_string(custom_field.id) or
|
|
||||||
(Map.get(cfv, :custom_field) &&
|
|
||||||
to_string(cfv.custom_field.id) == to_string(custom_field.id))
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp custom_field_sort?(field), do: String.starts_with?(field, @custom_field_prefix)
|
|
||||||
|
|
||||||
defp maybe_load_cycles(query, false, _show_current), do: query
|
|
||||||
|
|
||||||
defp maybe_load_cycles(query, true, show_current) do
|
|
||||||
MembershipFeeStatus.load_cycles_for_members(query, show_current)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp apply_cycle_status_filter(members, nil, _show_current), do: members
|
defp apply_cycle_status_filter(members, nil, _show_current), do: members
|
||||||
|
|
||||||
defp apply_cycle_status_filter(members, status, show_current) when status in [:paid, :unpaid] do
|
defp apply_cycle_status_filter(members, status, show_current) when status in [:paid, :unpaid] do
|
||||||
|
|
@ -298,20 +39,6 @@ defmodule Mv.Membership.MemberExport do
|
||||||
|
|
||||||
defp apply_cycle_status_filter(members, _status, _show_current), do: members
|
defp apply_cycle_status_filter(members, _status, _show_current), do: members
|
||||||
|
|
||||||
defp add_computed_fields(members, computed_fields, show_current_cycle) do
|
|
||||||
computed_fields = computed_fields || []
|
|
||||||
|
|
||||||
if "membership_fee_status" in computed_fields do
|
|
||||||
Enum.map(members, fn member ->
|
|
||||||
status = MembershipFeeStatus.get_cycle_status_for_member(member, show_current_cycle)
|
|
||||||
# <= Atom rein
|
|
||||||
Map.put(member, :membership_fee_status, status)
|
|
||||||
end)
|
|
||||||
else
|
|
||||||
members
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Called by controller to build parsed map from raw params (kept here so controller stays thin)
|
# Called by controller to build parsed map from raw params (kept here so controller stays thin)
|
||||||
@doc """
|
@doc """
|
||||||
Parses and validates export params (from JSON payload).
|
Parses and validates export params (from JSON payload).
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue