feat: remove birth_date field from Member resource
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
Users who need birthday data can use custom fields instead. Closes #161
This commit is contained in:
parent
40835f7a2d
commit
c8968636a8
10 changed files with 76 additions and 35 deletions
|
|
@ -115,7 +115,6 @@ Member (1) → (N) Properties
|
||||||
### Member Constraints
|
### Member Constraints
|
||||||
- First name and last name required (min 1 char)
|
- First name and last name required (min 1 char)
|
||||||
- Email unique, validated format (5-254 chars)
|
- Email unique, validated format (5-254 chars)
|
||||||
- Birth date cannot be in future
|
|
||||||
- Join date cannot be in future
|
- Join date cannot be in future
|
||||||
- Exit date must be after join date
|
- Exit date must be after join date
|
||||||
- Phone: `+?[0-9\- ]{6,20}`
|
- Phone: `+?[0-9\- ]{6,20}`
|
||||||
|
|
@ -169,7 +168,7 @@ Member (1) → (N) Properties
|
||||||
### Weighted Fields
|
### Weighted Fields
|
||||||
- **Weight A (highest):** first_name, last_name
|
- **Weight A (highest):** first_name, last_name
|
||||||
- **Weight B:** email, notes
|
- **Weight B:** email, notes
|
||||||
- **Weight C:** birth_date, phone_number, city, street, house_number, postal_code
|
- **Weight C:** phone_number, city, street, house_number, postal_code
|
||||||
- **Weight D (lowest):** join_date, exit_date
|
- **Weight D (lowest):** join_date, exit_date
|
||||||
|
|
||||||
### Usage Example
|
### Usage Example
|
||||||
|
|
@ -381,7 +380,7 @@ Install "DBML Language" extension to view/edit DBML files with:
|
||||||
- tokens (jti, purpose, extra_data)
|
- tokens (jti, purpose, extra_data)
|
||||||
|
|
||||||
**Personal Data (GDPR):**
|
**Personal Data (GDPR):**
|
||||||
- All member fields (name, email, birth_date, address)
|
- All member fields (name, email, address)
|
||||||
- User email
|
- User email
|
||||||
- Token subject
|
- Token subject
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -122,7 +122,6 @@ Table members {
|
||||||
first_name text [not null, note: 'Member first name (min length: 1)']
|
first_name text [not null, note: 'Member first name (min length: 1)']
|
||||||
last_name text [not null, note: 'Member last name (min length: 1)']
|
last_name text [not null, note: 'Member last name (min length: 1)']
|
||||||
email text [not null, unique, note: 'Member email address (5-254 chars, validated)']
|
email text [not null, unique, note: 'Member email address (5-254 chars, validated)']
|
||||||
birth_date date [null, note: 'Date of birth (cannot be in future)']
|
|
||||||
paid boolean [null, note: 'Payment status flag']
|
paid boolean [null, note: 'Payment status flag']
|
||||||
phone_number text [null, note: 'Contact phone number (format: +?[0-9\- ]{6,20})']
|
phone_number text [null, note: 'Contact phone number (format: +?[0-9\- ]{6,20})']
|
||||||
join_date date [null, note: 'Date when member joined club (cannot be in future)']
|
join_date date [null, note: 'Date when member joined club (cannot be in future)']
|
||||||
|
|
@ -153,7 +152,7 @@ Table members {
|
||||||
**Club Member Master Data**
|
**Club Member Master Data**
|
||||||
|
|
||||||
Core entity for membership management containing:
|
Core entity for membership management containing:
|
||||||
- Personal information (name, birth date, email)
|
- Personal information (name, email)
|
||||||
- Contact details (phone, address)
|
- Contact details (phone, address)
|
||||||
- Membership status (join/exit dates, payment status)
|
- Membership status (join/exit dates, payment status)
|
||||||
- Additional notes
|
- Additional notes
|
||||||
|
|
@ -183,7 +182,6 @@ Table members {
|
||||||
**Validation Rules:**
|
**Validation Rules:**
|
||||||
- first_name, last_name: min 1 character
|
- first_name, last_name: min 1 character
|
||||||
- email: 5-254 characters, valid email format
|
- email: 5-254 characters, valid email format
|
||||||
- birth_date: cannot be in future
|
|
||||||
- join_date: cannot be in future
|
- join_date: cannot be in future
|
||||||
- exit_date: must be after join_date (if both present)
|
- exit_date: must be after join_date (if both present)
|
||||||
- phone_number: matches pattern ^\+?[0-9\- ]{6,20}$
|
- phone_number: matches pattern ^\+?[0-9\- ]{6,20}$
|
||||||
|
|
|
||||||
|
|
@ -100,10 +100,10 @@
|
||||||
**Closed Issues:**
|
**Closed Issues:**
|
||||||
- [#194](https://git.local-it.org/local-it/mitgliederverwaltung/issues/194) - Custom Fields: Harden implementation (S)
|
- [#194](https://git.local-it.org/local-it/mitgliederverwaltung/issues/194) - Custom Fields: Harden implementation (S)
|
||||||
- [#197](https://git.local-it.org/local-it/mitgliederverwaltung/issues/197) - Custom Fields: Add option to show custom fields in member overview (M)
|
- [#197](https://git.local-it.org/local-it/mitgliederverwaltung/issues/197) - Custom Fields: Add option to show custom fields in member overview (M)
|
||||||
|
- [#161](https://git.local-it.org/local-it/mitgliederverwaltung/issues/161) - Remove birthday field from default configuration (S) - Closed 2025-12-02
|
||||||
|
|
||||||
**Open Issues:**
|
**Open Issues:**
|
||||||
- [#157](https://git.local-it.org/local-it/mitgliederverwaltung/issues/157) - Concept how custom fields are handled (M, High priority) [0/4 tasks]
|
- [#157](https://git.local-it.org/local-it/mitgliederverwaltung/issues/157) - Concept how custom fields are handled (M, High priority) [0/4 tasks]
|
||||||
- [#161](https://git.local-it.org/local-it/mitgliederverwaltung/issues/161) - Don't show birthday field for default configurations (S, Low priority)
|
|
||||||
- [#153](https://git.local-it.org/local-it/mitgliederverwaltung/issues/153) - Sorting functionalities for custom fields (M, Low priority)
|
- [#153](https://git.local-it.org/local-it/mitgliederverwaltung/issues/153) - Sorting functionalities for custom fields (M, Low priority)
|
||||||
|
|
||||||
**Missing Features:**
|
**Missing Features:**
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ defmodule Mv.Membership.Member do
|
||||||
- Email format validation (using EctoCommons.EmailValidator)
|
- Email format validation (using EctoCommons.EmailValidator)
|
||||||
- Phone number format: international format with 6-20 digits
|
- Phone number format: international format with 6-20 digits
|
||||||
- Postal code format: exactly 5 digits (German format)
|
- Postal code format: exactly 5 digits (German format)
|
||||||
- Date validations: birth_date and join_date not in future, exit_date after join_date
|
- Date validations: join_date not in future, exit_date after join_date
|
||||||
- Email uniqueness: prevents conflicts with unlinked users
|
- Email uniqueness: prevents conflicts with unlinked users
|
||||||
|
|
||||||
## Full-Text Search
|
## Full-Text Search
|
||||||
|
|
@ -284,11 +284,6 @@ defmodule Mv.Membership.Member do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Birth date not in the future
|
|
||||||
validate compare(:birth_date, less_than_or_equal_to: &Date.utc_today/0),
|
|
||||||
where: [present(:birth_date)],
|
|
||||||
message: "cannot be in the future"
|
|
||||||
|
|
||||||
# Join date not in the future
|
# Join date not in the future
|
||||||
validate compare(:join_date, less_than_or_equal_to: &Date.utc_today/0),
|
validate compare(:join_date, less_than_or_equal_to: &Date.utc_today/0),
|
||||||
where: [present(:join_date)],
|
where: [present(:join_date)],
|
||||||
|
|
@ -351,10 +346,6 @@ defmodule Mv.Membership.Member do
|
||||||
constraints min_length: 5, max_length: 254
|
constraints min_length: 5, max_length: 254
|
||||||
end
|
end
|
||||||
|
|
||||||
attribute :birth_date, :date do
|
|
||||||
allow_nil? true
|
|
||||||
end
|
|
||||||
|
|
||||||
attribute :paid, :boolean do
|
attribute :paid, :boolean do
|
||||||
allow_nil? true
|
allow_nil? true
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ defmodule Mv.Constants do
|
||||||
:first_name,
|
:first_name,
|
||||||
:last_name,
|
:last_name,
|
||||||
:email,
|
:email,
|
||||||
:birth_date,
|
|
||||||
:paid,
|
:paid,
|
||||||
:phone_number,
|
:phone_number,
|
||||||
:join_date,
|
:join_date,
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ defmodule MvWeb.MemberLive.Form do
|
||||||
- first_name, last_name, email
|
- first_name, last_name, email
|
||||||
|
|
||||||
**Optional:**
|
**Optional:**
|
||||||
- birth_date, phone_number, address fields (city, street, house_number, postal_code)
|
- phone_number, address fields (city, street, house_number, postal_code)
|
||||||
- join_date, exit_date
|
- join_date, exit_date
|
||||||
- paid status
|
- paid status
|
||||||
- notes
|
- notes
|
||||||
|
|
@ -45,7 +45,6 @@ defmodule MvWeb.MemberLive.Form do
|
||||||
<.input field={@form[:first_name]} label={gettext("First Name")} required />
|
<.input field={@form[:first_name]} label={gettext("First Name")} required />
|
||||||
<.input field={@form[:last_name]} label={gettext("Last Name")} required />
|
<.input field={@form[:last_name]} label={gettext("Last Name")} required />
|
||||||
<.input field={@form[:email]} label={gettext("Email")} required type="email" />
|
<.input field={@form[:email]} label={gettext("Email")} required type="email" />
|
||||||
<.input field={@form[:birth_date]} label={gettext("Birth Date")} type="date" />
|
|
||||||
<.input field={@form[:paid]} label={gettext("Paid")} type="checkbox" />
|
<.input field={@form[:paid]} label={gettext("Paid")} type="checkbox" />
|
||||||
<.input field={@form[:phone_number]} label={gettext("Phone Number")} />
|
<.input field={@form[:phone_number]} label={gettext("Phone Number")} />
|
||||||
<.input field={@form[:join_date]} label={gettext("Join Date")} type="date" />
|
<.input field={@form[:join_date]} label={gettext("Join Date")} type="date" />
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ defmodule MvWeb.MemberLive.Show do
|
||||||
- Return to member list
|
- Return to member list
|
||||||
|
|
||||||
## Displayed Information
|
## Displayed Information
|
||||||
- Basic: name, email, dates (birth, join, exit)
|
- Basic: name, email, dates (join, exit)
|
||||||
- Contact: phone number
|
- Contact: phone number
|
||||||
- Address: street, house number, postal code, city
|
- Address: street, house number, postal code, city
|
||||||
- Status: paid flag
|
- Status: paid flag
|
||||||
|
|
@ -48,7 +48,6 @@ defmodule MvWeb.MemberLive.Show do
|
||||||
<:item title={gettext("First Name")}>{@member.first_name}</:item>
|
<:item title={gettext("First Name")}>{@member.first_name}</:item>
|
||||||
<:item title={gettext("Last Name")}>{@member.last_name}</:item>
|
<:item title={gettext("Last Name")}>{@member.last_name}</:item>
|
||||||
<:item title={gettext("Email")}>{@member.email}</:item>
|
<:item title={gettext("Email")}>{@member.email}</:item>
|
||||||
<:item title={gettext("Birth Date")}>{@member.birth_date}</:item>
|
|
||||||
<:item title={gettext("Paid")}>
|
<:item title={gettext("Paid")}>
|
||||||
{if @member.paid, do: gettext("Yes"), else: gettext("No")}
|
{if @member.paid, do: gettext("Yes"), else: gettext("No")}
|
||||||
</:item>
|
</:item>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,69 @@
|
||||||
|
defmodule Mv.Repo.Migrations.RemoveBirthDateFromMembers do
|
||||||
|
@moduledoc """
|
||||||
|
Removes the birth_date column from the members table.
|
||||||
|
|
||||||
|
The birth_date field has been removed from the application because most users
|
||||||
|
don't record birthday data. Users who need this can use a custom field instead.
|
||||||
|
|
||||||
|
This migration also updates the search_vector trigger to remove birth_date.
|
||||||
|
"""
|
||||||
|
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
def up do
|
||||||
|
# Update the trigger function to remove birth_date from search_vector
|
||||||
|
execute("""
|
||||||
|
CREATE OR REPLACE FUNCTION members_search_vector_trigger() RETURNS trigger AS $$
|
||||||
|
BEGIN
|
||||||
|
NEW.search_vector :=
|
||||||
|
setweight(to_tsvector('simple', coalesce(NEW.first_name, '')), 'A') ||
|
||||||
|
setweight(to_tsvector('simple', coalesce(NEW.last_name, '')), 'A') ||
|
||||||
|
setweight(to_tsvector('simple', coalesce(NEW.email, '')), 'B') ||
|
||||||
|
setweight(to_tsvector('simple', coalesce(NEW.phone_number, '')), 'C') ||
|
||||||
|
setweight(to_tsvector('simple', coalesce(NEW.join_date::text, '')), 'D') ||
|
||||||
|
setweight(to_tsvector('simple', coalesce(NEW.exit_date::text, '')), 'D') ||
|
||||||
|
setweight(to_tsvector('simple', coalesce(NEW.notes, '')), 'B') ||
|
||||||
|
setweight(to_tsvector('simple', coalesce(NEW.city, '')), 'C') ||
|
||||||
|
setweight(to_tsvector('simple', coalesce(NEW.street, '')), 'C') ||
|
||||||
|
setweight(to_tsvector('simple', coalesce(NEW.house_number::text, '')), 'C') ||
|
||||||
|
setweight(to_tsvector('simple', coalesce(NEW.postal_code::text, '')), 'C');
|
||||||
|
RETURN NEW;
|
||||||
|
END
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Remove the birth_date column
|
||||||
|
alter table(:members) do
|
||||||
|
remove :birth_date
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def down do
|
||||||
|
# Add the birth_date column back
|
||||||
|
alter table(:members) do
|
||||||
|
add :birth_date, :date
|
||||||
|
end
|
||||||
|
|
||||||
|
# Restore the trigger function with birth_date
|
||||||
|
execute("""
|
||||||
|
CREATE OR REPLACE FUNCTION members_search_vector_trigger() RETURNS trigger AS $$
|
||||||
|
BEGIN
|
||||||
|
NEW.search_vector :=
|
||||||
|
setweight(to_tsvector('simple', coalesce(NEW.first_name, '')), 'A') ||
|
||||||
|
setweight(to_tsvector('simple', coalesce(NEW.last_name, '')), 'A') ||
|
||||||
|
setweight(to_tsvector('simple', coalesce(NEW.email, '')), 'B') ||
|
||||||
|
setweight(to_tsvector('simple', coalesce(NEW.birth_date::text, '')), 'C') ||
|
||||||
|
setweight(to_tsvector('simple', coalesce(NEW.phone_number, '')), 'C') ||
|
||||||
|
setweight(to_tsvector('simple', coalesce(NEW.join_date::text, '')), 'D') ||
|
||||||
|
setweight(to_tsvector('simple', coalesce(NEW.exit_date::text, '')), 'D') ||
|
||||||
|
setweight(to_tsvector('simple', coalesce(NEW.notes, '')), 'B') ||
|
||||||
|
setweight(to_tsvector('simple', coalesce(NEW.city, '')), 'C') ||
|
||||||
|
setweight(to_tsvector('simple', coalesce(NEW.street, '')), 'C') ||
|
||||||
|
setweight(to_tsvector('simple', coalesce(NEW.house_number::text, '')), 'C') ||
|
||||||
|
setweight(to_tsvector('simple', coalesce(NEW.postal_code::text, '')), 'C');
|
||||||
|
RETURN NEW;
|
||||||
|
END
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
""")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -112,7 +112,6 @@ for member_attrs <- [
|
||||||
first_name: "Hans",
|
first_name: "Hans",
|
||||||
last_name: "Müller",
|
last_name: "Müller",
|
||||||
email: "hans.mueller@example.de",
|
email: "hans.mueller@example.de",
|
||||||
birth_date: ~D[1985-06-15],
|
|
||||||
join_date: ~D[2023-01-15],
|
join_date: ~D[2023-01-15],
|
||||||
paid: true,
|
paid: true,
|
||||||
phone_number: "+49301234567",
|
phone_number: "+49301234567",
|
||||||
|
|
@ -125,7 +124,6 @@ for member_attrs <- [
|
||||||
first_name: "Greta",
|
first_name: "Greta",
|
||||||
last_name: "Schmidt",
|
last_name: "Schmidt",
|
||||||
email: "greta.schmidt@example.de",
|
email: "greta.schmidt@example.de",
|
||||||
birth_date: ~D[1990-03-22],
|
|
||||||
join_date: ~D[2023-02-01],
|
join_date: ~D[2023-02-01],
|
||||||
paid: false,
|
paid: false,
|
||||||
phone_number: "+49309876543",
|
phone_number: "+49309876543",
|
||||||
|
|
@ -139,7 +137,6 @@ for member_attrs <- [
|
||||||
first_name: "Friedrich",
|
first_name: "Friedrich",
|
||||||
last_name: "Wagner",
|
last_name: "Wagner",
|
||||||
email: "friedrich.wagner@example.de",
|
email: "friedrich.wagner@example.de",
|
||||||
birth_date: ~D[1978-11-08],
|
|
||||||
join_date: ~D[2022-11-10],
|
join_date: ~D[2022-11-10],
|
||||||
paid: true,
|
paid: true,
|
||||||
phone_number: "+49301122334",
|
phone_number: "+49301122334",
|
||||||
|
|
@ -151,7 +148,6 @@ for member_attrs <- [
|
||||||
first_name: "Marianne",
|
first_name: "Marianne",
|
||||||
last_name: "Wagner",
|
last_name: "Wagner",
|
||||||
email: "marianne.wagner@example.de",
|
email: "marianne.wagner@example.de",
|
||||||
birth_date: ~D[1978-11-08],
|
|
||||||
join_date: ~D[2022-11-10],
|
join_date: ~D[2022-11-10],
|
||||||
paid: true,
|
paid: true,
|
||||||
phone_number: "+49301122334",
|
phone_number: "+49301122334",
|
||||||
|
|
@ -186,7 +182,6 @@ linked_members = [
|
||||||
first_name: "Maria",
|
first_name: "Maria",
|
||||||
last_name: "Weber",
|
last_name: "Weber",
|
||||||
email: "maria.weber@example.de",
|
email: "maria.weber@example.de",
|
||||||
birth_date: ~D[1992-07-14],
|
|
||||||
join_date: ~D[2023-03-15],
|
join_date: ~D[2023-03-15],
|
||||||
paid: true,
|
paid: true,
|
||||||
phone_number: "+49301357924",
|
phone_number: "+49301357924",
|
||||||
|
|
@ -202,7 +197,6 @@ linked_members = [
|
||||||
first_name: "Thomas",
|
first_name: "Thomas",
|
||||||
last_name: "Klein",
|
last_name: "Klein",
|
||||||
email: "thomas.klein@example.de",
|
email: "thomas.klein@example.de",
|
||||||
birth_date: ~D[1988-12-03],
|
|
||||||
join_date: ~D[2023-04-01],
|
join_date: ~D[2023-04-01],
|
||||||
paid: false,
|
paid: false,
|
||||||
phone_number: "+49302468135",
|
phone_number: "+49302468135",
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ defmodule Mv.Membership.MemberTest do
|
||||||
@valid_attrs %{
|
@valid_attrs %{
|
||||||
first_name: "John",
|
first_name: "John",
|
||||||
last_name: "Doe",
|
last_name: "Doe",
|
||||||
birth_date: ~D[1990-01-01],
|
|
||||||
paid: true,
|
paid: true,
|
||||||
email: "john@example.com",
|
email: "john@example.com",
|
||||||
phone_number: "+49123456789",
|
phone_number: "+49123456789",
|
||||||
|
|
@ -43,12 +42,6 @@ defmodule Mv.Membership.MemberTest do
|
||||||
assert error_message(errors, :email) =~ "is not a valid email"
|
assert error_message(errors, :email) =~ "is not a valid email"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "Birth date is optional but must not be in the future" do
|
|
||||||
attrs = Map.put(@valid_attrs, :birth_date, Date.utc_today() |> Date.add(1))
|
|
||||||
assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs)
|
|
||||||
assert error_message(errors, :birth_date) =~ "cannot be in the future"
|
|
||||||
end
|
|
||||||
|
|
||||||
test "Paid is optional but must be boolean if specified" do
|
test "Paid is optional but must be boolean if specified" do
|
||||||
attrs = Map.put(@valid_attrs, :paid, nil)
|
attrs = Map.put(@valid_attrs, :paid, nil)
|
||||||
attrs2 = Map.put(@valid_attrs, :paid, "yes")
|
attrs2 = Map.put(@valid_attrs, :paid, "yes")
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue