diff --git a/app/conference_query.py b/app/conference_query.py new file mode 100644 index 0000000..b661ede --- /dev/null +++ b/app/conference_query.py @@ -0,0 +1,28 @@ +""" +Query helpers for Conference-related aggregates. + +Peers created only for addConnection (P2P remote) are linked on conference.participants +but often have no Session and no name; they are excluded. SFU endpoints (is_sfu) are +excluded from participant totals — only human/clients with a session in the conference +are counted. + +participants_count must use a Subquery (not Count on the outer queryset's participants +relation): when /v1/conferences is filtered by participantId, the outer query joins +participants and would otherwise restrict the aggregate to that single participant. +""" + +from django.db.models import Count, IntegerField, OuterRef, Subquery + +from .models.participant import Participant + +PARTICIPANTS_COUNT_SUBQUERY = Subquery( + Participant.objects.filter( + conferences=OuterRef('pk'), + is_active=True, + is_sfu=False, + sessions__conference_id=OuterRef('pk'), + ).order_by().values('conferences').annotate( + cnt=Count('id', distinct=True), + ).values('cnt')[:1], + output_field=IntegerField(), +) diff --git a/app/tests/test_pr26_regressions.py b/app/tests/test_pr26_regressions.py index 7e47b65..4106285 100644 --- a/app/tests/test_pr26_regressions.py +++ b/app/tests/test_pr26_regressions.py @@ -141,3 +141,41 @@ def test_gum_summary_skips_non_dict_issue_data(self): self.assertEqual(payload["total"], 1) self.assertEqual(payload["data"][0]["name"], "NotAllowedError") self.assertEqual(payload["data"][0]["count"], 1) + + def test_conferences_participant_id_filter_preserves_full_participants_count(self): + """ + When the conference list is filtered to one person (participantId), + participants_count must still reflect everyone in that conference, not 1. + """ + conference = Conference.objects.create( + conference_id="conf-multi", + app=self.app, + ) + participants = [] + for i in range(3): + p = Participant.objects.create( + participant_id=f"user-{i}", + app=self.app, + ) + p.conferences.add(conference) + Session.objects.create( + conference=conference, + participant=p, + ) + participants.append(p) + + response = self.client.get( + "/v1/conferences", + { + "appId": str(self.app.id), + "participantId": str(participants[0].id), + "limit": "50", + }, + ) + + self.assertEqual(response.status_code, 200) + payload = json.loads(response.content) + self.assertEqual(payload["count"], 1) + self.assertEqual(len(payload["results"]), 1) + self.assertEqual(payload["results"][0]["id"], str(conference.id)) + self.assertEqual(payload["results"][0]["participants_count"], 3) diff --git a/app/views/conference_participant_count_summary_view.py b/app/views/conference_participant_count_summary_view.py index 188d916..eed51f3 100644 --- a/app/views/conference_participant_count_summary_view.py +++ b/app/views/conference_participant_count_summary_view.py @@ -2,13 +2,11 @@ from collections import Counter from django.core.exceptions import ValidationError -from django.db.models import Count, IntegerField, OuterRef, Subquery - +from ..conference_query import PARTICIPANTS_COUNT_SUBQUERY from ..errors import INVALID_PARAMETERS, MISSING_PARAMETERS, PMError from ..summary_cache import cached_json from ..utils import JSONHttpResponse from ..models.conference import Conference -from ..models.participant import Participant from .generic_view import GenericView @@ -50,17 +48,7 @@ def compute(): try: per_conf = (Conference.objects .filter(**filters) - .annotate( - participants_count=Subquery( - Participant.objects.filter( - conferences=OuterRef('pk'), - is_active=True, - ).order_by().values('conferences').annotate( - cnt=Count('id', distinct=True) - ).values('cnt')[:1], - output_field=IntegerField(), - ), - ) + .annotate(participants_count=PARTICIPANTS_COUNT_SUBQUERY) .values_list('participants_count', flat=True)) except ValidationError: raise PMError(status=400, app_error=INVALID_PARAMETERS) diff --git a/app/views/conference_view.py b/app/views/conference_view.py index 722899e..6c02fe3 100644 --- a/app/views/conference_view.py +++ b/app/views/conference_view.py @@ -1,14 +1,14 @@ import datetime from django.core.exceptions import ValidationError -from django.db.models import Count, Exists, IntegerField, OuterRef, Subquery +from django.db.models import Exists, OuterRef +from ..conference_query import PARTICIPANTS_COUNT_SUBQUERY from ..errors import (INVALID_PARAMETERS, CONFERENCE_NOT_FOUND, MISSING_PARAMETERS, PMError) from ..utils import JSONHttpResponse, serialize, paginate_and_serialize from ..models.conference import Conference from ..models.issue import Issue -from ..models.participant import Participant from .generic_view import GenericView class ConferencesView(GenericView): @@ -63,15 +63,7 @@ def filter(cls, request): has_warnings=Exists( Issue.objects.filter(conference=OuterRef('pk'), type='w', is_active=True) ), - participants_count=Subquery( - Participant.objects.filter( - conferences=OuterRef('pk'), - is_active=True, - ).order_by().values('conferences').annotate( - cnt=Count('id', distinct=True) - ).values('cnt')[:1], - output_field=IntegerField(), - ), + participants_count=PARTICIPANTS_COUNT_SUBQUERY, ) if filter_by_issue_code: objs = objs.distinct()