Improve Join description handling for GDPR/DSGVO #521
4 changed files with 242 additions and 3 deletions
|
|
@ -12,6 +12,8 @@ defmodule Mv.Membership.CustomField do
|
|||
- `slug` - URL-friendly, immutable identifier automatically generated from name (e.g., "phone-mobile")
|
||||
- `value_type` - Data type constraint (`:string`, `:integer`, `:boolean`, `:date`, `:email`). Immutable after creation.
|
||||
- `description` - Optional human-readable description
|
||||
- `join_description` - Optional label shown for this field on the public join form
|
||||
(e.g., a GDPR confirmation text); supports inline external links. Falls back to `name` when nil.
|
||||
- `required` - If true, all members must have this custom field (future feature)
|
||||
- `show_in_overview` - If true, this custom field will be displayed in the member overview table and can be sorted
|
||||
|
||||
|
|
@ -61,7 +63,14 @@ defmodule Mv.Membership.CustomField do
|
|||
end
|
||||
|
||||
actions do
|
||||
default_accept [:name, :value_type, :description, :required, :show_in_overview]
|
||||
default_accept [
|
||||
:name,
|
||||
:value_type,
|
||||
:description,
|
||||
:join_description,
|
||||
:required,
|
||||
:show_in_overview
|
||||
]
|
||||
|
||||
read :read do
|
||||
primary? true
|
||||
|
|
@ -69,13 +78,13 @@ defmodule Mv.Membership.CustomField do
|
|||
end
|
||||
|
||||
create :create do
|
||||
accept [:name, :value_type, :description, :required, :show_in_overview]
|
||||
accept [:name, :value_type, :description, :join_description, :required, :show_in_overview]
|
||||
change Mv.Membership.Changes.GenerateSlug
|
||||
validate string_length(:slug, min: 1)
|
||||
end
|
||||
|
||||
update :update do
|
||||
accept [:name, :description, :required, :show_in_overview]
|
||||
accept [:name, :description, :join_description, :required, :show_in_overview]
|
||||
require_atomic? false
|
||||
|
||||
validate fn changeset, _context ->
|
||||
|
|
@ -139,6 +148,15 @@ defmodule Mv.Membership.CustomField do
|
|||
trim?: true
|
||||
]
|
||||
|
||||
attribute :join_description, :string,
|
||||
allow_nil?: true,
|
||||
public?: true,
|
||||
description: "Label shown for this field on the public join form; supports external links",
|
||||
constraints: [
|
||||
max_length: 1000,
|
||||
trim?: true
|
||||
]
|
||||
|
||||
attribute :required, :boolean,
|
||||
default: false,
|
||||
allow_nil?: false
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
defmodule Mv.Repo.Migrations.AddJoinDescriptionToCustomFields do
|
||||
@moduledoc """
|
||||
Updates resources based on their most recent snapshots.
|
||||
|
||||
This file was autogenerated with `mix ash_postgres.generate_migrations`
|
||||
"""
|
||||
|
||||
use Ecto.Migration
|
||||
|
||||
def up do
|
||||
alter table(:custom_fields) do
|
||||
add :join_description, :text
|
||||
end
|
||||
end
|
||||
|
||||
def down do
|
||||
alter table(:custom_fields) do
|
||||
remove :join_description
|
||||
end
|
||||
end
|
||||
end
|
||||
145
priv/resource_snapshots/repo/custom_fields/20260603000204.json
Normal file
145
priv/resource_snapshots/repo/custom_fields/20260603000204.json
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
{
|
||||
"attributes": [
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "fragment(\"gen_random_uuid()\")",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": true,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "id",
|
||||
"type": "uuid"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "name",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "slug",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "value_type",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "description",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "join_description",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "false",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "required",
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "true",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "show_in_overview",
|
||||
"type": "boolean"
|
||||
}
|
||||
],
|
||||
"base_filter": null,
|
||||
"check_constraints": [],
|
||||
"create_table_options": null,
|
||||
"custom_indexes": [],
|
||||
"custom_statements": [],
|
||||
"has_create_action": true,
|
||||
"hash": "2600667D140A2A846F9A848ACEFCADA1F1206950B38EF407B0BB13816E508A2A",
|
||||
"identities": [
|
||||
{
|
||||
"all_tenants?": false,
|
||||
"base_filter": null,
|
||||
"index_name": "custom_fields_unique_name_index",
|
||||
"keys": [
|
||||
{
|
||||
"type": "atom",
|
||||
"value": "name"
|
||||
}
|
||||
],
|
||||
"name": "unique_name",
|
||||
"nils_distinct?": true,
|
||||
"where": null
|
||||
},
|
||||
{
|
||||
"all_tenants?": false,
|
||||
"base_filter": null,
|
||||
"index_name": "custom_fields_unique_slug_index",
|
||||
"keys": [
|
||||
{
|
||||
"type": "atom",
|
||||
"value": "slug"
|
||||
}
|
||||
],
|
||||
"name": "unique_slug",
|
||||
"nils_distinct?": true,
|
||||
"where": null
|
||||
}
|
||||
],
|
||||
"multitenancy": {
|
||||
"attribute": null,
|
||||
"global": null,
|
||||
"strategy": null
|
||||
},
|
||||
"repo": "Elixir.Mv.Repo",
|
||||
"schema": null,
|
||||
"table": "custom_fields"
|
||||
}
|
||||
|
|
@ -159,6 +159,61 @@ defmodule Mv.Membership.CustomFieldValidationTest do
|
|||
end
|
||||
end
|
||||
|
||||
describe "join_description" do
|
||||
test "persists join_description when set", %{actor: actor} do
|
||||
assert {:ok, custom_field} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "dsgvo_field",
|
||||
value_type: :boolean,
|
||||
join_description: "hereby I confirm the GDPR"
|
||||
})
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
assert custom_field.join_description == "hereby I confirm the GDPR"
|
||||
end
|
||||
|
||||
test "defaults to nil when not given", %{actor: actor} do
|
||||
assert {:ok, custom_field} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "no_join_desc",
|
||||
value_type: :boolean
|
||||
})
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
assert custom_field.join_description == nil
|
||||
end
|
||||
|
||||
test "rejects join_description longer than 1000 characters", %{actor: actor} do
|
||||
assert {:error, changeset} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "too_long_join_desc",
|
||||
value_type: :boolean,
|
||||
join_description: String.duplicate("a", 1001)
|
||||
})
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
assert [%{field: :join_description, message: message}] = changeset.errors
|
||||
assert message =~ "max" or message =~ "length" or message =~ "1000"
|
||||
end
|
||||
|
||||
test "is writable via the update action", %{actor: actor} do
|
||||
{:ok, custom_field} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{name: "updatable", value_type: :boolean})
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
assert {:ok, updated} =
|
||||
custom_field
|
||||
|> Ash.Changeset.for_update(:update, %{join_description: "Accept the GDPR"})
|
||||
|> Ash.update(actor: actor)
|
||||
|
||||
assert updated.join_description == "Accept the GDPR"
|
||||
end
|
||||
end
|
||||
|
||||
describe "name uniqueness" do
|
||||
test "rejects duplicate names", %{actor: actor} do
|
||||
assert {:ok, _} =
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue