CustomField Resource Policies closes #386 #387
5 changed files with 67 additions and 19 deletions
|
|
@ -486,6 +486,24 @@ defmodule Mv.Membership.Member do
|
||||||
build_custom_field_validation_error(missing_fields)
|
build_custom_field_validation_error(missing_fields)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
{:error, %Ash.Error.Forbidden{}} ->
|
||||||
|
Logger.warning(
|
||||||
|
"Required custom fields validation: actor not authorized to read CustomField"
|
||||||
|
)
|
||||||
|
|
||||||
|
{:error,
|
||||||
|
field: :custom_field_values,
|
||||||
|
message:
|
||||||
|
"You are not authorized to perform this action. Please sign in again or contact support."}
|
||||||
|
|
||||||
|
{:error, :missing_actor} ->
|
||||||
|
Logger.warning("Required custom fields validation: no actor in context")
|
||||||
|
|
||||||
|
{:error,
|
||||||
|
field: :custom_field_values,
|
||||||
|
message:
|
||||||
|
"You are not authorized to perform this action. Please sign in again or contact support."}
|
||||||
|
|
||||||
{:error, error} ->
|
{:error, error} ->
|
||||||
Logger.error(
|
Logger.error(
|
||||||
"Failed to load custom fields for validation: #{inspect(error)}. Required field validation cannot be performed."
|
"Failed to load custom fields for validation: #{inspect(error)}. Required field validation cannot be performed."
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ defmodule Mv.Membership do
|
||||||
The domain exposes these main actions:
|
The domain exposes these main actions:
|
||||||
- Member CRUD: `create_member/1`, `list_members/0`, `update_member/2`, `destroy_member/1`
|
- Member CRUD: `create_member/1`, `list_members/0`, `update_member/2`, `destroy_member/1`
|
||||||
- Custom field value management: `create_custom_field_value/1`, `list_custom_field_values/0`, etc.
|
- Custom field value management: `create_custom_field_value/1`, `list_custom_field_values/0`, etc.
|
||||||
- Custom field management: `create_custom_field/1`, `list_custom_fields/0`, `list_required_custom_fields/0`, etc.
|
- Custom field management: `create_custom_field/1`, `list_custom_fields/0`, `list_required_custom_fields/1`, etc.
|
||||||
- Settings management: `get_settings/0`, `update_settings/2`, `update_member_field_visibility/2`, `update_single_member_field_visibility/3`
|
- Settings management: `get_settings/0`, `update_settings/2`, `update_member_field_visibility/2`, `update_single_member_field_visibility/3`
|
||||||
- Group management: `create_group/1`, `list_groups/0`, `update_group/2`, `destroy_group/1`
|
- Group management: `create_group/1`, `list_groups/0`, `update_group/2`, `destroy_group/1`
|
||||||
- Member-group associations: `create_member_group/1`, `list_member_groups/0`, `destroy_member_group/1`
|
- Member-group associations: `create_member_group/1`, `list_member_groups/0`, `destroy_member_group/1`
|
||||||
|
|
@ -156,7 +156,7 @@ defmodule Mv.Membership do
|
||||||
|
|
||||||
This is an optimized version that filters at the database level instead of
|
This is an optimized version that filters at the database level instead of
|
||||||
loading all custom fields and filtering in memory. Requires an actor for
|
loading all custom fields and filtering in memory. Requires an actor for
|
||||||
authorization (CustomField read policy).
|
authorization (CustomField read policy). Callers must pass `actor:`; no default.
|
||||||
|
|
||||||
## Options
|
## Options
|
||||||
|
|
||||||
|
|
@ -166,22 +166,26 @@ defmodule Mv.Membership do
|
||||||
## Returns
|
## Returns
|
||||||
|
|
||||||
- `{:ok, required_custom_fields}` - List of required custom fields
|
- `{:ok, required_custom_fields}` - List of required custom fields
|
||||||
- `{:error, error}` - Error reading custom fields (e.g. Forbidden when no actor)
|
- `{:error, :missing_actor}` - When actor is nil (caller must pass actor)
|
||||||
|
- `{:error, error}` - Error reading custom fields (e.g. Forbidden)
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
iex> {:ok, required_fields} = Mv.Membership.list_required_custom_fields(actor: actor)
|
iex> {:ok, required_fields} = Mv.Membership.list_required_custom_fields(actor: actor)
|
||||||
iex> Enum.all?(required_fields, & &1.required)
|
iex> Enum.all?(required_fields, & &1.required)
|
||||||
true
|
true
|
||||||
"""
|
|
||||||
def list_required_custom_fields(opts \\ []) do
|
|
||||||
actor = Keyword.get(opts, :actor)
|
|
||||||
|
|
||||||
|
iex> Mv.Membership.list_required_custom_fields(actor: nil)
|
||||||
|
{:error, :missing_actor}
|
||||||
|
"""
|
||||||
|
def list_required_custom_fields(actor: actor) when not is_nil(actor) do
|
||||||
Mv.Membership.CustomField
|
Mv.Membership.CustomField
|
||||||
|> Ash.Query.filter(expr(required == true))
|
|> Ash.Query.filter(expr(required == true))
|
||||||
|> Ash.read(domain: __MODULE__, actor: actor)
|
|> Ash.read(domain: __MODULE__, actor: actor)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def list_required_custom_fields(actor: nil), do: {:error, :missing_actor}
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Updates the member field visibility configuration.
|
Updates the member field visibility configuration.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,8 @@ defmodule MvWeb.CustomFieldLive.IndexComponent do
|
||||||
"""
|
"""
|
||||||
use MvWeb, :live_component
|
use MvWeb, :live_component
|
||||||
|
|
||||||
|
require Logger
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def render(assigns) do
|
def render(assigns) do
|
||||||
assigns = assign(assigns, :field_type_label, &MvWeb.Translations.FieldTypes.label/1)
|
assigns = assign(assigns, :field_type_label, &MvWeb.Translations.FieldTypes.label/1)
|
||||||
|
|
@ -177,10 +179,18 @@ defmodule MvWeb.CustomFieldLive.IndexComponent do
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
|
|
||||||
defp stream_custom_fields(actor) do
|
defp stream_custom_fields(actor, parent_pid) do
|
||||||
case Ash.read(Mv.Membership.CustomField, actor: actor) do
|
case Ash.read(Mv.Membership.CustomField, actor: actor) do
|
||||||
{:ok, custom_fields} -> custom_fields
|
{:ok, custom_fields} ->
|
||||||
{:error, _} -> []
|
custom_fields
|
||||||
|
|
||||||
|
{:error, error} ->
|
||||||
|
Logger.warning(
|
||||||
|
"CustomFieldLive.IndexComponent: failed to load custom fields: #{inspect(error)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
send(parent_pid, {:custom_fields_load_error, error})
|
||||||
|
[]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -215,7 +225,7 @@ defmodule MvWeb.CustomFieldLive.IndexComponent do
|
||||||
|> assign_new(:show_delete_modal, fn -> false end)
|
|> assign_new(:show_delete_modal, fn -> false end)
|
||||||
|> assign_new(:custom_field_to_delete, fn -> nil end)
|
|> assign_new(:custom_field_to_delete, fn -> nil end)
|
||||||
|> assign_new(:slug_confirmation, fn -> "" end)
|
|> assign_new(:slug_confirmation, fn -> "" end)
|
||||||
|> stream(:custom_fields, stream_custom_fields(assigns[:actor]), reset: true)}
|
|> stream(:custom_fields, stream_custom_fields(assigns[:actor], self()), reset: true)}
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
|
|
|
||||||
|
|
@ -504,6 +504,15 @@ defmodule MvWeb.GlobalSettingsLive do
|
||||||
{:noreply, put_flash(socket, :error, gettext("Slug does not match. Deletion cancelled."))}
|
{:noreply, put_flash(socket, :error, gettext("Slug does not match. Deletion cancelled."))}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def handle_info({:custom_fields_load_error, _error}, socket) do
|
||||||
|
{:noreply,
|
||||||
|
put_flash(
|
||||||
|
socket,
|
||||||
|
:error,
|
||||||
|
gettext("Could not load data fields. Please check your permissions.")
|
||||||
|
)}
|
||||||
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_info({:editing_section_changed, section}, socket) do
|
def handle_info({:editing_section_changed, section}, socket) do
|
||||||
{:noreply, assign(socket, :active_editing_section, section)}
|
{:noreply, assign(socket, :active_editing_section, section)}
|
||||||
|
|
|
||||||
|
|
@ -381,12 +381,16 @@ Enum.each(member_attrs_list, fn member_attrs ->
|
||||||
final_member =
|
final_member =
|
||||||
if is_nil(member.membership_fee_type_id) and
|
if is_nil(member.membership_fee_type_id) and
|
||||||
Map.has_key?(member_attrs_without_status, :membership_fee_type_id) do
|
Map.has_key?(member_attrs_without_status, :membership_fee_type_id) do
|
||||||
member
|
{:ok, updated} =
|
||||||
|> Ash.Changeset.for_update(:update_member, %{
|
Membership.update_member(
|
||||||
membership_fee_type_id: member_attrs_without_status.membership_fee_type_id
|
member,
|
||||||
})
|
%{
|
||||||
|> Ash.Changeset.put_context(:actor, admin_user_with_role)
|
membership_fee_type_id: member_attrs_without_status.membership_fee_type_id
|
||||||
|> Ash.update!(actor: admin_user_with_role)
|
},
|
||||||
|
actor: admin_user_with_role
|
||||||
|
)
|
||||||
|
|
||||||
|
updated
|
||||||
else
|
else
|
||||||
member
|
member
|
||||||
end
|
end
|
||||||
|
|
@ -546,9 +550,12 @@ Enum.with_index(linked_members)
|
||||||
fee_type_index = rem(3 + index, length(all_fee_types))
|
fee_type_index = rem(3 + index, length(all_fee_types))
|
||||||
fee_type = Enum.at(all_fee_types, fee_type_index)
|
fee_type = Enum.at(all_fee_types, fee_type_index)
|
||||||
|
|
||||||
member
|
{:ok, updated} =
|
||||||
|> Ash.Changeset.for_update(:update_member, %{membership_fee_type_id: fee_type.id})
|
Membership.update_member(member, %{membership_fee_type_id: fee_type.id},
|
||||||
|> Ash.update!(actor: admin_user_with_role)
|
actor: admin_user_with_role
|
||||||
|
)
|
||||||
|
|
||||||
|
updated
|
||||||
else
|
else
|
||||||
member
|
member
|
||||||
end
|
end
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue