fix: adress review comments
Some checks failed
continuous-integration/drone/push Build is failing

This commit is contained in:
Simon 2026-02-18 13:05:31 +01:00
parent f6575319f7
commit 63b8e70e62
Signed by: simon
GPG key ID: 40E7A58C4AA1EDB2
4 changed files with 83 additions and 77 deletions

View file

@ -233,9 +233,8 @@ Settings (1) → MembershipFeeType (0..1)
## Full-Text Search
### Implementation
- **Trigger:** `members_search_vector_trigger()` on `members` (INSERT/UPDATE)
- **Trigger:** `update_member_search_vector_from_member_groups()` on `member_groups` (INSERT/UPDATE/DELETE)
- **Function:** Automatically updates `search_vector` on member and member_groups changes
- **Trigger** on `members` (INSERT/UPDATE): runs function `members_search_vector_trigger()`
- **Trigger** on `member_groups` (INSERT/UPDATE/DELETE): `update_member_search_vector_on_member_groups_change` runs function `update_member_search_vector_from_member_groups()`
- **Index Type:** GIN (Generalized Inverted Index)
### Weighted Fields

View file

@ -32,7 +32,7 @@ defmodule Mv.Repo.Migrations.AddGroupNamesToMemberSearchVector do
FROM custom_field_values
WHERE member_id = NEW.id AND value IS NOT NULL;
-- Aggregate group names for this member (weight B, same as city/notes)
-- Aggregate group names for this member (weight B, same as notes/email)
SELECT string_agg(g.name, ' ')
INTO groups_text
FROM member_groups mg
@ -162,7 +162,8 @@ defmodule Mv.Repo.Migrations.AddGroupNamesToMemberSearchVector do
$$ LANGUAGE plpgsql;
""")
# 3. Trigger on member_groups: when associations change, refresh that member's search_vector
# 3. Trigger on member_groups: when associations change, refresh affected member(s) search_vector.
# On UPDATE with different member_id, refresh both OLD and NEW member so neither keeps a stale vector.
execute("""
CREATE FUNCTION update_member_search_vector_from_member_groups() RETURNS trigger AS $$
DECLARE
@ -180,66 +181,71 @@ defmodule Mv.Repo.Migrations.AddGroupNamesToMemberSearchVector do
custom_values_text text;
groups_text text;
BEGIN
member_id_val := COALESCE(NEW.member_id, OLD.member_id);
FOR member_id_val IN
SELECT COALESCE(NEW.member_id, OLD.member_id)
UNION ALL
SELECT OLD.member_id
WHERE TG_OP = 'UPDATE' AND OLD.member_id IS DISTINCT FROM NEW.member_id
LOOP
SELECT
first_name,
last_name,
email,
join_date,
exit_date,
notes,
city,
street,
house_number,
postal_code
INTO
member_first_name,
member_last_name,
member_email,
member_join_date,
member_exit_date,
member_notes,
member_city,
member_street,
member_house_number,
member_postal_code
FROM members
WHERE id = member_id_val;
SELECT
first_name,
last_name,
email,
join_date,
exit_date,
notes,
city,
street,
house_number,
postal_code
INTO
member_first_name,
member_last_name,
member_email,
member_join_date,
member_exit_date,
member_notes,
member_city,
member_street,
member_house_number,
member_postal_code
FROM members
WHERE id = member_id_val;
SELECT string_agg(
CASE
WHEN value ? '_union_value' THEN value->>'_union_value'
WHEN value ? 'value' THEN value->>'value'
ELSE ''
END,
' '
)
INTO custom_values_text
FROM custom_field_values
WHERE member_id = member_id_val AND value IS NOT NULL;
SELECT string_agg(
CASE
WHEN value ? '_union_value' THEN value->>'_union_value'
WHEN value ? 'value' THEN value->>'value'
ELSE ''
END,
' '
)
INTO custom_values_text
FROM custom_field_values
WHERE member_id = member_id_val AND value IS NOT NULL;
SELECT string_agg(g.name, ' ')
INTO groups_text
FROM member_groups mg
JOIN groups g ON g.id = mg.group_id
WHERE mg.member_id = member_id_val;
SELECT string_agg(g.name, ' ')
INTO groups_text
FROM member_groups mg
JOIN groups g ON g.id = mg.group_id
WHERE mg.member_id = member_id_val;
UPDATE members
SET search_vector =
setweight(to_tsvector('simple', coalesce(member_first_name, '')), 'A') ||
setweight(to_tsvector('simple', coalesce(member_last_name, '')), 'A') ||
setweight(to_tsvector('simple', coalesce(member_email, '')), 'B') ||
setweight(to_tsvector('simple', coalesce(member_join_date::text, '')), 'D') ||
setweight(to_tsvector('simple', coalesce(member_exit_date::text, '')), 'D') ||
setweight(to_tsvector('simple', coalesce(member_notes, '')), 'B') ||
setweight(to_tsvector('simple', coalesce(member_city, '')), 'C') ||
setweight(to_tsvector('simple', coalesce(member_street, '')), 'C') ||
setweight(to_tsvector('simple', coalesce(member_house_number::text, '')), 'C') ||
setweight(to_tsvector('simple', coalesce(member_postal_code::text, '')), 'C') ||
setweight(to_tsvector('simple', coalesce(custom_values_text, '')), 'C') ||
setweight(to_tsvector('simple', coalesce(groups_text, '')), 'B')
WHERE id = member_id_val;
UPDATE members
SET search_vector =
setweight(to_tsvector('simple', coalesce(member_first_name, '')), 'A') ||
setweight(to_tsvector('simple', coalesce(member_last_name, '')), 'A') ||
setweight(to_tsvector('simple', coalesce(member_email, '')), 'B') ||
setweight(to_tsvector('simple', coalesce(member_join_date::text, '')), 'D') ||
setweight(to_tsvector('simple', coalesce(member_exit_date::text, '')), 'D') ||
setweight(to_tsvector('simple', coalesce(member_notes, '')), 'B') ||
setweight(to_tsvector('simple', coalesce(member_city, '')), 'C') ||
setweight(to_tsvector('simple', coalesce(member_street, '')), 'C') ||
setweight(to_tsvector('simple', coalesce(member_house_number::text, '')), 'C') ||
setweight(to_tsvector('simple', coalesce(member_postal_code::text, '')), 'C') ||
setweight(to_tsvector('simple', coalesce(custom_values_text, '')), 'C') ||
setweight(to_tsvector('simple', coalesce(groups_text, '')), 'B')
WHERE id = member_id_val;
END LOOP;
RETURN COALESCE(NEW, OLD);
END

View file

@ -273,7 +273,9 @@ defmodule Mv.Membership.MemberSearchGroupsIntegrationTest do
end
describe "edge cases" do
test "partial group name matches via FTS", %{system_actor: actor} do
test "token match: single word in group name matches (e.g. Board in Board Members)", %{
system_actor: actor
} do
{:ok, member} =
Mv.Membership.create_member(
%{first_name: "Partial", last_name: "Test", email: "partial@example.com"},
@ -300,7 +302,9 @@ defmodule Mv.Membership.MemberSearchGroupsIntegrationTest do
"Search for 'Board' should find member in group 'Board Members'"
end
test "special characters in group name do not break search", %{system_actor: actor} do
test "search with token from group name containing special characters does not crash", %{
system_actor: actor
} do
{:ok, member} =
Mv.Membership.create_member(
%{first_name: "Special", last_name: "Char", email: "special@example.com"},
@ -317,7 +321,7 @@ defmodule Mv.Membership.MemberSearchGroupsIntegrationTest do
|> Ash.Changeset.for_create(:create, %{member_id: member.id, group_id: group.id})
|> Ash.create(actor: actor)
# Search should not crash; at least exact or word match should work
# Search for a token from the group name; proves tokenization does not crash on "A&B"
results =
Member
|> Member.fuzzy_search(%{query: "Team"})

View file

@ -15,10 +15,12 @@ defmodule MvWeb.MemberLive.IndexGroupsIntegrationTest do
import Phoenix.LiveViewTest
require Ash.Query
alias Mv.Membership.{Group, MemberGroup, CustomField, CustomFieldValue}
alias Mv.Helpers.SystemActor
alias Mv.Membership.{CustomField, CustomFieldValue, Group, MemberGroup}
alias Mv.MembershipFees.{MembershipFeeCycle, MembershipFeeType}
setup do
system_actor = Mv.Helpers.SystemActor.get_system_actor()
system_actor = SystemActor.get_system_actor()
# Create test members
{:ok, member1} =
@ -81,15 +83,10 @@ defmodule MvWeb.MemberLive.IndexGroupsIntegrationTest do
conn = conn_with_oidc_user(conn)
{:ok, _view, html} = live(conn, "/members")
# Verify groups column is visible by default
# Verify groups column is visible by default (header and content)
assert html =~ group1.name
assert html =~ member1.first_name
# Hide groups column via field visibility dropdown
# (This tests integration with field visibility feature)
# Note: Actual implementation depends on how field visibility works
# For now, we verify the column exists and can be toggled
assert html
assert html =~ "Groups"
end
test "groups filter works with custom field filters", %{
@ -141,11 +138,11 @@ defmodule MvWeb.MemberLive.IndexGroupsIntegrationTest do
member1: member1,
group1: group1
} do
system_actor = Mv.Helpers.SystemActor.get_system_actor()
system_actor = SystemActor.get_system_actor()
# Create a membership fee type and cycle for member1
{:ok, fee_type} =
Mv.MembershipFees.MembershipFeeType
MembershipFeeType
|> Ash.Changeset.for_create(:create, %{
name: "Test Fee",
amount: Decimal.new("50.00"),
@ -160,7 +157,7 @@ defmodule MvWeb.MemberLive.IndexGroupsIntegrationTest do
)
{:ok, _cycle} =
Mv.MembershipFees.MembershipFeeCycle
MembershipFeeCycle
|> Ash.Changeset.for_create(:create, %{
member_id: member1.id,
membership_fee_type_id: fee_type.id,