This commit is contained in:
parent
f6575319f7
commit
63b8e70e62
4 changed files with 83 additions and 77 deletions
|
|
@ -233,9 +233,8 @@ Settings (1) → MembershipFeeType (0..1)
|
||||||
## Full-Text Search
|
## Full-Text Search
|
||||||
|
|
||||||
### Implementation
|
### Implementation
|
||||||
- **Trigger:** `members_search_vector_trigger()` on `members` (INSERT/UPDATE)
|
- **Trigger** on `members` (INSERT/UPDATE): runs function `members_search_vector_trigger()`
|
||||||
- **Trigger:** `update_member_search_vector_from_member_groups()` on `member_groups` (INSERT/UPDATE/DELETE)
|
- **Trigger** on `member_groups` (INSERT/UPDATE/DELETE): `update_member_search_vector_on_member_groups_change` runs function `update_member_search_vector_from_member_groups()`
|
||||||
- **Function:** Automatically updates `search_vector` on member and member_groups changes
|
|
||||||
- **Index Type:** GIN (Generalized Inverted Index)
|
- **Index Type:** GIN (Generalized Inverted Index)
|
||||||
|
|
||||||
### Weighted Fields
|
### Weighted Fields
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ defmodule Mv.Repo.Migrations.AddGroupNamesToMemberSearchVector do
|
||||||
FROM custom_field_values
|
FROM custom_field_values
|
||||||
WHERE member_id = NEW.id AND value IS NOT NULL;
|
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, ' ')
|
SELECT string_agg(g.name, ' ')
|
||||||
INTO groups_text
|
INTO groups_text
|
||||||
FROM member_groups mg
|
FROM member_groups mg
|
||||||
|
|
@ -162,7 +162,8 @@ defmodule Mv.Repo.Migrations.AddGroupNamesToMemberSearchVector do
|
||||||
$$ LANGUAGE plpgsql;
|
$$ 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("""
|
execute("""
|
||||||
CREATE FUNCTION update_member_search_vector_from_member_groups() RETURNS trigger AS $$
|
CREATE FUNCTION update_member_search_vector_from_member_groups() RETURNS trigger AS $$
|
||||||
DECLARE
|
DECLARE
|
||||||
|
|
@ -180,8 +181,12 @@ defmodule Mv.Repo.Migrations.AddGroupNamesToMemberSearchVector do
|
||||||
custom_values_text text;
|
custom_values_text text;
|
||||||
groups_text text;
|
groups_text text;
|
||||||
BEGIN
|
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
|
SELECT
|
||||||
first_name,
|
first_name,
|
||||||
last_name,
|
last_name,
|
||||||
|
|
@ -240,6 +245,7 @@ defmodule Mv.Repo.Migrations.AddGroupNamesToMemberSearchVector do
|
||||||
setweight(to_tsvector('simple', coalesce(custom_values_text, '')), 'C') ||
|
setweight(to_tsvector('simple', coalesce(custom_values_text, '')), 'C') ||
|
||||||
setweight(to_tsvector('simple', coalesce(groups_text, '')), 'B')
|
setweight(to_tsvector('simple', coalesce(groups_text, '')), 'B')
|
||||||
WHERE id = member_id_val;
|
WHERE id = member_id_val;
|
||||||
|
END LOOP;
|
||||||
|
|
||||||
RETURN COALESCE(NEW, OLD);
|
RETURN COALESCE(NEW, OLD);
|
||||||
END
|
END
|
||||||
|
|
|
||||||
|
|
@ -273,7 +273,9 @@ defmodule Mv.Membership.MemberSearchGroupsIntegrationTest do
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "edge cases" do
|
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} =
|
{:ok, member} =
|
||||||
Mv.Membership.create_member(
|
Mv.Membership.create_member(
|
||||||
%{first_name: "Partial", last_name: "Test", email: "partial@example.com"},
|
%{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'"
|
"Search for 'Board' should find member in group 'Board Members'"
|
||||||
end
|
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} =
|
{:ok, member} =
|
||||||
Mv.Membership.create_member(
|
Mv.Membership.create_member(
|
||||||
%{first_name: "Special", last_name: "Char", email: "special@example.com"},
|
%{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.Changeset.for_create(:create, %{member_id: member.id, group_id: group.id})
|
||||||
|> Ash.create(actor: actor)
|
|> 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 =
|
results =
|
||||||
Member
|
Member
|
||||||
|> Member.fuzzy_search(%{query: "Team"})
|
|> Member.fuzzy_search(%{query: "Team"})
|
||||||
|
|
|
||||||
|
|
@ -15,10 +15,12 @@ defmodule MvWeb.MemberLive.IndexGroupsIntegrationTest do
|
||||||
import Phoenix.LiveViewTest
|
import Phoenix.LiveViewTest
|
||||||
require Ash.Query
|
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
|
setup do
|
||||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
system_actor = SystemActor.get_system_actor()
|
||||||
|
|
||||||
# Create test members
|
# Create test members
|
||||||
{:ok, member1} =
|
{:ok, member1} =
|
||||||
|
|
@ -81,15 +83,10 @@ defmodule MvWeb.MemberLive.IndexGroupsIntegrationTest do
|
||||||
conn = conn_with_oidc_user(conn)
|
conn = conn_with_oidc_user(conn)
|
||||||
{:ok, _view, html} = live(conn, "/members")
|
{: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 =~ group1.name
|
||||||
assert html =~ member1.first_name
|
assert html =~ member1.first_name
|
||||||
|
assert html =~ "Groups"
|
||||||
# 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
|
|
||||||
end
|
end
|
||||||
|
|
||||||
test "groups filter works with custom field filters", %{
|
test "groups filter works with custom field filters", %{
|
||||||
|
|
@ -141,11 +138,11 @@ defmodule MvWeb.MemberLive.IndexGroupsIntegrationTest do
|
||||||
member1: member1,
|
member1: member1,
|
||||||
group1: group1
|
group1: group1
|
||||||
} do
|
} do
|
||||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
system_actor = SystemActor.get_system_actor()
|
||||||
|
|
||||||
# Create a membership fee type and cycle for member1
|
# Create a membership fee type and cycle for member1
|
||||||
{:ok, fee_type} =
|
{:ok, fee_type} =
|
||||||
Mv.MembershipFees.MembershipFeeType
|
MembershipFeeType
|
||||||
|> Ash.Changeset.for_create(:create, %{
|
|> Ash.Changeset.for_create(:create, %{
|
||||||
name: "Test Fee",
|
name: "Test Fee",
|
||||||
amount: Decimal.new("50.00"),
|
amount: Decimal.new("50.00"),
|
||||||
|
|
@ -160,7 +157,7 @@ defmodule MvWeb.MemberLive.IndexGroupsIntegrationTest do
|
||||||
)
|
)
|
||||||
|
|
||||||
{:ok, _cycle} =
|
{:ok, _cycle} =
|
||||||
Mv.MembershipFees.MembershipFeeCycle
|
MembershipFeeCycle
|
||||||
|> Ash.Changeset.for_create(:create, %{
|
|> Ash.Changeset.for_create(:create, %{
|
||||||
member_id: member1.id,
|
member_id: member1.id,
|
||||||
membership_fee_type_id: fee_type.id,
|
membership_fee_type_id: fee_type.id,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue