Rationalize news notifications and the Home news teaser for multi-child families

A family with several kids on a news item's audience now gets one email/one
Notification, not one per child -- deduped on resolved recipient emails, so
overlapping (not just identical) guardian sets still collapse correctly.
Event notifications are untouched: each child still needs their own reply.

Home's "Club news" teaser is also decoupled from the person-scope switcher --
a parent with no team of their own still sees their kids' team news when
they've picked their own "Me" chip, not just when "All" is selected.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ECGMEwrc2k4D8VQuwjstj9
This commit is contained in:
2026-08-22 16:09:36 +02:00
parent 62c36f47b9
commit 2b6a4d21bb
4 changed files with 94 additions and 6 deletions

View File

@@ -381,6 +381,28 @@ class HomeViewTests(TestCase):
self.assertEqual(list(response.context["news_items"]), [])
def test_news_teaser_includes_a_managed_childs_team_news_even_when_scoped_to_me(self):
# A parent with no team of their own, scoped to their own "Me" chip
# (no team membership -> empty team_ids for `people`) should still see
# news about a child's team -- news isn't a per-person action like
# RSVP/dues, so it's keyed off every managed person, not just scope.
family = Family.objects.create(name="Bakker")
FamilyMembership.objects.create(family=family, member=self.member, role=FamilyMembership.FamilyRole.PARENT)
child = Member.objects.create(first_name="Noor", last_name="Bakker")
FamilyMembership.objects.create(family=family, member=child, role=FamilyMembership.FamilyRole.CHILD)
ClubMembership.objects.create(club=self.club, member=child, season=self.season)
team = Team.objects.create(club=self.club, name="U12", short_name="U12")
position = Position.objects.create(club=self.club, name="Forward", short_name="F")
TeamMembership.objects.create(team=team, member=child, season=self.season, position=position)
team_news = News.objects.create(club=self.club, title="U12 news", body="Body.", status=News.Status.PUBLISHED, published_at=timezone.now())
team_news.teams.add(team)
self.client.force_login(self.user)
response = self._get(url=reverse("mobile:home") + f"?as={self.member.pk}")
self.assertEqual(response.context["scope_person"], self.member)
self.assertIn(team_news, response.context["news_items"])
def test_news_card_shows_an_empty_state_with_a_link_to_all_news_when_there_is_none(self):
self.client.force_login(self.user)

View File

@@ -240,9 +240,14 @@ class HomeView(PersonScopeMixin, LoginRequiredMixin, TemplateView):
season = current_season(self.request.club)
if season is not None:
dues_rows = open_dues_rows(self.request.club, people, season)
team_ids = list(TeamMembership.objects.filter(member__in=people, season=season).values_list("team_id", flat=True))
# Deliberately self.managed_people, not the scoped `people` above --
# news isn't a per-person action like RSVP/dues, it's "things this
# account should know about", so a parent picking their own "Me"
# chip (no team of their own) still sees their kids' team news
# rather than only club-wide items.
news_team_ids = list(TeamMembership.objects.filter(member__in=self.managed_people, season=season).values_list("team_id", flat=True))
else:
team_ids = []
news_team_ids = []
news_items = list(
News.objects.filter(
@@ -251,7 +256,7 @@ class HomeView(PersonScopeMixin, LoginRequiredMixin, TemplateView):
published_at__lte=now,
visibility__in=[News.Visibility.INTERNAL, News.Visibility.BOTH],
)
.filter(Q(teams__isnull=True) | Q(teams__id__in=team_ids))
.filter(Q(teams__isnull=True) | Q(teams__id__in=news_team_ids))
.prefetch_related("teams")
.order_by("-published_at")
.distinct()[: self.NEWS_LIMIT]