fix linting
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/promote/production Build is passing

This commit is contained in:
carla 2026-02-09 14:08:12 +01:00
parent 80fe73a561
commit e68a7cf8c7
4 changed files with 456 additions and 390 deletions

View file

@ -55,14 +55,7 @@ defmodule Mv.Membership.MemberExport do
case Ash.read(query, actor: actor) do case Ash.read(query, actor: actor) do
{:ok, custom_fields} -> {:ok, custom_fields} ->
by_id = by_id = build_custom_fields_by_id(custom_field_ids, custom_fields)
Enum.reduce(custom_field_ids, %{}, fn id, acc ->
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)
{:ok, by_id} {:ok, by_id}
{:error, %Ash.Error.Forbidden{}} -> {:error, %Ash.Error.Forbidden{}} ->
@ -70,33 +63,72 @@ defmodule Mv.Membership.MemberExport do
end end
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 defp build_column_specs(parsed, custom_fields_by_id) do
member_specs = 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 -> Enum.map(parsed.member_fields, fn f ->
if f in parsed.selectable_member_fields do build_single_member_spec(f, parsed.selectable_member_fields)
%{kind: :member_field, key: f} 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 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 # only allow known computed export fields to avoid crashing on unknown atoms
if f in @computed_export_fields do if field in @computed_export_fields do
%{kind: :computed, key: String.to_existing_atom(f)} %{kind: :computed, key: String.to_existing_atom(field)}
else else
# ignore unknown non-selectable fields defensively # ignore unknown non-selectable fields defensively
nil nil
end end
end end
end)
|> Enum.reject(&is_nil/1)
custom_specs = defp build_custom_column_specs(parsed, custom_fields_by_id) do
parsed.custom_field_ids parsed.custom_field_ids
|> Enum.map(fn id -> Map.get(custom_fields_by_id, id) end) |> Enum.map(fn id -> Map.get(custom_fields_by_id, id) end)
|> Enum.reject(&is_nil/1) |> Enum.reject(&is_nil/1)
|> Enum.map(fn cf -> %{kind: :custom_field, key: cf.id, custom_field: cf} end) |> Enum.map(fn cf -> %{kind: :custom_field, key: cf.id, custom_field: cf} end)
member_specs ++ custom_specs
end end
defp load_members(actor, parsed, custom_fields_by_id) do 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 = select_fields =
[:id] ++ Enum.map(parsed.selectable_member_fields, &String.to_existing_atom/1) [:id] ++ Enum.map(parsed.selectable_member_fields, &String.to_existing_atom/1)
@ -114,7 +146,6 @@ defmodule Mv.Membership.MemberExport do
|> load_custom_field_values_query(custom_field_ids_union) |> load_custom_field_values_query(custom_field_ids_union)
|> maybe_load_cycles(need_cycles, parsed.show_current_cycle) |> maybe_load_cycles(need_cycles, parsed.show_current_cycle)
query =
if parsed.selected_ids != [] do if parsed.selected_ids != [] do
Ash.Query.filter(query, expr(id in ^parsed.selected_ids)) Ash.Query.filter(query, expr(id in ^parsed.selected_ids))
else else
@ -125,10 +156,16 @@ defmodule Mv.Membership.MemberExport do
q q
end) end)
end end
end
case Ash.read(query, actor: actor) do defp process_loaded_members(members, parsed, custom_fields_by_id) do
{:ok, members} -> members
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 if parsed.selected_ids == [] do
members members
|> apply_cycle_status_filter(parsed.cycle_status_filter, parsed.show_current_cycle) |> apply_cycle_status_filter(parsed.cycle_status_filter, parsed.show_current_cycle)
@ -139,8 +176,9 @@ defmodule Mv.Membership.MemberExport do
else else
members members
end end
end
members = defp apply_post_load_sorting(members, parsed, custom_fields_by_id) do
if parsed.selected_ids == [] and sort_after_load?(parsed.sort_field) do if parsed.selected_ids == [] and sort_after_load?(parsed.sort_field) do
sort_members_by_custom_field( sort_members_by_custom_field(
members, members,
@ -151,15 +189,6 @@ defmodule Mv.Membership.MemberExport do
else else
members members
end end
# Calculate membership_fee_status for computed fields
members = add_computed_fields(members, parsed.computed_fields, parsed.show_current_cycle)
{:ok, members}
{:error, %Ash.Error.Forbidden{}} ->
{:error, :forbidden}
end
end end
defp load_custom_field_values_query(query, []), do: query defp load_custom_field_values_query(query, []), do: query
@ -259,7 +288,8 @@ defmodule Mv.Membership.MemberExport do
if "membership_fee_status" in computed_fields do if "membership_fee_status" in computed_fields do
Enum.map(members, fn member -> Enum.map(members, fn member ->
status = MembershipFeeStatus.get_cycle_status_for_member(member, show_current_cycle) status = MembershipFeeStatus.get_cycle_status_for_member(member, show_current_cycle)
Map.put(member, :membership_fee_status, status) # <= Atom rein # <= Atom rein
Map.put(member, :membership_fee_status, status)
end) end)
else else
members members
@ -333,8 +363,9 @@ defmodule Mv.Membership.MemberExport do
case Map.get(params, "boolean_filters") do case Map.get(params, "boolean_filters") do
map when is_map(map) -> map when is_map(map) ->
map map
|> Enum.filter(fn {k, v} -> is_binary(k) and is_boolean(v) end) |> Enum.filter(fn {k, v} ->
|> Enum.filter(fn {k, _} -> match?({:ok, _}, Ecto.UUID.cast(k)) end) is_binary(k) and is_boolean(v) and match?({:ok, _}, Ecto.UUID.cast(k))
end)
|> Enum.into(%{}) |> Enum.into(%{})
_ -> _ ->

View file

@ -96,16 +96,19 @@ defmodule MvWeb.MemberExportController do
defp filter_existing_atoms(list) when is_list(list) do defp filter_existing_atoms(list) when is_list(list) do
list list
|> Enum.filter(&is_binary/1)
|> Enum.filter(fn name -> |> Enum.filter(fn name ->
is_binary(name) and atom_exists?(name)
end)
|> Enum.uniq()
end
defp atom_exists?(name) do
try do try do
_ = String.to_existing_atom(name) _ = String.to_existing_atom(name)
true true
rescue rescue
ArgumentError -> false ArgumentError -> false
end end
end)
|> Enum.uniq()
end end
defp extract_list(params, key) do defp extract_list(params, key) do
@ -215,11 +218,15 @@ defmodule MvWeb.MemberExportController do
defp build_custom_fields_by_id(custom_field_ids, custom_fields) do defp build_custom_fields_by_id(custom_field_ids, custom_fields) do
Enum.reduce(custom_field_ids, %{}, fn id, acc -> 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 case Enum.find(custom_fields, fn cf -> to_string(cf.id) == to_string(id) end) do
nil -> acc nil -> acc
cf -> Map.put(acc, id, cf) cf -> Map.put(acc, id, cf)
end end
end)
end end
defp load_members_for_export(actor, parsed, custom_fields_by_id) do defp load_members_for_export(actor, parsed, custom_fields_by_id) do
@ -322,12 +329,10 @@ defmodule MvWeb.MemberExportController do
defp maybe_sort_export(query, _field, nil), do: {query, false} defp maybe_sort_export(query, _field, nil), do: {query, false}
defp maybe_sort_export(query, field, order) when is_binary(field) do defp maybe_sort_export(query, field, order) when is_binary(field) do
cond do if custom_field_sort?(field) do
custom_field_sort?(field) ->
# Custom field sort → in-memory nach dem Read (wie Tabelle) # Custom field sort → in-memory nach dem Read (wie Tabelle)
{query, true} {query, true}
else
true ->
field_atom = String.to_existing_atom(field) field_atom = String.to_existing_atom(field)
if field_atom in (Mv.Constants.member_fields() -- [:notes]) do if field_atom in (Mv.Constants.member_fields() -- [:notes]) do
@ -372,12 +377,7 @@ defmodule MvWeb.MemberExportController do
sorted_with_values = sorted_with_values =
Enum.sort_by(with_values, fn member -> Enum.sort_by(with_values, fn member ->
member extract_member_sort_value(member, custom_field)
|> find_cfv(custom_field)
|> case do
nil -> nil
cfv -> extract_sort_value(cfv.value, custom_field.value_type)
end
end) end)
sorted_with_values = sorted_with_values =
@ -415,6 +415,13 @@ defmodule MvWeb.MemberExportController do
end) end)
end end
defp extract_member_sort_value(member, custom_field) do
case find_cfv(member, custom_field) do
nil -> nil
cfv -> extract_sort_value(cfv.value, custom_field.value_type)
end
end
defp build_columns(conn, parsed, custom_fields_by_id) do defp build_columns(conn, parsed, custom_fields_by_id) do
member_cols = member_cols =
Enum.map(parsed.selectable_member_fields, fn field -> Enum.map(parsed.selectable_member_fields, fn field ->

View file

@ -245,7 +245,10 @@ defmodule MvWeb.MemberLive.Index do
new_show_current, new_show_current,
socket.assigns.boolean_custom_field_filters socket.assigns.boolean_custom_field_filters
) )
|> maybe_add_field_selection(socket.assigns[:user_field_selection], socket.assigns[:fields_in_url?] || false) |> maybe_add_field_selection(
socket.assigns[:user_field_selection],
socket.assigns[:fields_in_url?] || false
)
new_path = ~p"/members?#{query_params}" new_path = ~p"/members?#{query_params}"
@ -352,7 +355,10 @@ defmodule MvWeb.MemberLive.Index do
socket.assigns.show_current_cycle, socket.assigns.show_current_cycle,
socket.assigns.boolean_custom_field_filters socket.assigns.boolean_custom_field_filters
) )
|> maybe_add_field_selection(socket.assigns[:user_field_selection], socket.assigns[:fields_in_url?] || false) |> maybe_add_field_selection(
socket.assigns[:user_field_selection],
socket.assigns[:fields_in_url?] || false
)
{:noreply, push_patch(socket, to: ~p"/members?#{query_params}", replace: true)} {:noreply, push_patch(socket, to: ~p"/members?#{query_params}", replace: true)}
end end
@ -374,7 +380,10 @@ defmodule MvWeb.MemberLive.Index do
socket.assigns.show_current_cycle, socket.assigns.show_current_cycle,
socket.assigns.boolean_custom_field_filters socket.assigns.boolean_custom_field_filters
) )
|> maybe_add_field_selection(socket.assigns[:user_field_selection], socket.assigns[:fields_in_url?] || false) |> maybe_add_field_selection(
socket.assigns[:user_field_selection],
socket.assigns[:fields_in_url?] || false
)
new_path = ~p"/members?#{query_params}" new_path = ~p"/members?#{query_params}"
@ -398,7 +407,10 @@ defmodule MvWeb.MemberLive.Index do
socket.assigns.show_current_cycle, socket.assigns.show_current_cycle,
socket.assigns.boolean_custom_field_filters socket.assigns.boolean_custom_field_filters
) )
|> maybe_add_field_selection(socket.assigns[:user_field_selection], socket.assigns[:fields_in_url?] || false) |> maybe_add_field_selection(
socket.assigns[:user_field_selection],
socket.assigns[:fields_in_url?] || false
)
new_path = ~p"/members?#{query_params}" new_path = ~p"/members?#{query_params}"
{:noreply, push_patch(socket, to: new_path, replace: true)} {:noreply, push_patch(socket, to: new_path, replace: true)}
@ -428,7 +440,10 @@ defmodule MvWeb.MemberLive.Index do
socket.assigns.show_current_cycle, socket.assigns.show_current_cycle,
updated_filters updated_filters
) )
|> maybe_add_field_selection(socket.assigns[:user_field_selection], socket.assigns[:fields_in_url?] || false) |> maybe_add_field_selection(
socket.assigns[:user_field_selection],
socket.assigns[:fields_in_url?] || false
)
new_path = ~p"/members?#{query_params}" new_path = ~p"/members?#{query_params}"
{:noreply, push_patch(socket, to: new_path, replace: true)} {:noreply, push_patch(socket, to: new_path, replace: true)}
@ -452,7 +467,10 @@ defmodule MvWeb.MemberLive.Index do
socket.assigns.show_current_cycle, socket.assigns.show_current_cycle,
boolean_filters boolean_filters
) )
|> maybe_add_field_selection(socket.assigns[:user_field_selection], socket.assigns[:fields_in_url?] || false) |> maybe_add_field_selection(
socket.assigns[:user_field_selection],
socket.assigns[:fields_in_url?] || false
)
new_path = ~p"/members?#{query_params}" new_path = ~p"/members?#{query_params}"
{:noreply, push_patch(socket, to: new_path, replace: true)} {:noreply, push_patch(socket, to: new_path, replace: true)}
@ -542,6 +560,7 @@ defmodule MvWeb.MemberLive.Index do
@impl true @impl true
def handle_params(params, _url, socket) do def handle_params(params, _url, socket) do
prev_sig = build_signature(socket) prev_sig = build_signature(socket)
fields_in_url? = fields_in_url? =
case Map.get(params, "fields") do case Map.get(params, "fields") do
v when is_binary(v) and v != "" -> true v when is_binary(v) and v != "" -> true
@ -691,9 +710,10 @@ defmodule MvWeb.MemberLive.Index do
defp maybe_add_field_selection(params, selection, true) when is_map(selection) do defp maybe_add_field_selection(params, selection, true) when is_map(selection) do
fields_param = FieldSelection.to_url_param(selection) fields_param = FieldSelection.to_url_param(selection)
cond do if fields_param == "" do
fields_param == "" -> Map.delete(params, "fields") Map.delete(params, "fields")
true -> Map.put(params, "fields", fields_param) else
Map.put(params, "fields", fields_param)
end end
end end
@ -900,15 +920,23 @@ defmodule MvWeb.MemberLive.Index do
defp maybe_sort(query, _field, nil, _custom_fields), do: {query, false} defp maybe_sort(query, _field, nil, _custom_fields), do: {query, false}
defp maybe_sort(query, field, order, _custom_fields) do defp maybe_sort(query, field, order, _custom_fields) do
if computed_field?(field) do
{query, false}
else
apply_sort_to_query(query, field, order)
end
end
defp computed_field?(field) do
computed_atoms = FieldVisibility.computed_member_fields() computed_atoms = FieldVisibility.computed_member_fields()
computed_strings = Enum.map(computed_atoms, &Atom.to_string/1) computed_strings = Enum.map(computed_atoms, &Atom.to_string/1)
cond do
# Block computed fields (atom and string variants)
(is_atom(field) and field in computed_atoms) or (is_atom(field) and field in computed_atoms) or
(is_binary(field) and field in computed_strings) -> (is_binary(field) and field in computed_strings)
{query, false} end
defp apply_sort_to_query(query, field, order) do
cond do
# Custom field sort -> after load # Custom field sort -> after load
custom_field_sort?(field) -> custom_field_sort?(field) ->
{query, true} {query, true}

View file

@ -274,7 +274,7 @@ defmodule MvWeb.MemberExportControllerTest do
|> Ash.Changeset.for_create(:create, %{ |> Ash.Changeset.for_create(:create, %{
member_id: member_with_integer.id, member_id: member_with_integer.id,
custom_field_id: integer_field.id, custom_field_id: integer_field.id,
value: 12345 value: 12_345
}) })
|> Ash.create(actor: system_actor) |> Ash.create(actor: system_actor)