fix(epg): improve parameter descriptions and enhance title whole word matching tests. Fix bug with postgres whole words (they use \y not \b for regex)

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
SergeantPanda 2026-04-24 15:46:56 -05:00
parent dbbc2f5735
commit c486f641a8
2 changed files with 26 additions and 12 deletions

View file

@ -183,15 +183,15 @@ class ProgramViewSet(viewsets.ModelViewSet):
OpenApiTypes.STR,
description='Title search query. Supports AND/OR operators (case-insensitive), quoted phrases, and parentheses. Double-quote a phrase to match it literally: `"Law and Order"`. Unquoted space-separated terms are matched as a phrase; use AND/OR to combine separate terms.',
),
OpenApiParameter('title_regex', OpenApiTypes.BOOL, description='Enable regex matching for title (case-insensitive). Example: `^The` matches titles starting with "The".'),
OpenApiParameter('title_whole_words', OpenApiTypes.BOOL, description='Match whole words only in title. Prevents "NEW" from matching "News".'),
OpenApiParameter('title_regex', OpenApiTypes.BOOL, description='Enable regex matching for title (case-insensitive, default: false). e.g. `^The` matches titles starting with "The".'),
OpenApiParameter('title_whole_words', OpenApiTypes.BOOL, description='Match whole words only in title (default: false). e.g. `new` matches "Newcastle" normally but not with whole words enabled.'),
OpenApiParameter(
'description',
OpenApiTypes.STR,
description='Description search query. Same syntax and features as title search.'
),
OpenApiParameter('description_regex', OpenApiTypes.BOOL, description='Enable regex matching for description (case-insensitive).'),
OpenApiParameter('description_whole_words', OpenApiTypes.BOOL, description='Match whole words only in description.'),
OpenApiParameter('description_regex', OpenApiTypes.BOOL, description='Enable regex matching for description (case-insensitive, default: false).'),
OpenApiParameter('description_whole_words', OpenApiTypes.BOOL, description='Match whole words only in description (default: false). Same behaviour as title_whole_words.'),
OpenApiParameter('start_after', OpenApiTypes.DATETIME, description='Filter programs starting at or after this time. ISO 8601 format, e.g. `2026-02-14T18:00:00Z`.'),
OpenApiParameter('start_before', OpenApiTypes.DATETIME, description='Filter programs starting at or before this time. ISO 8601 format.'),
OpenApiParameter('end_after', OpenApiTypes.DATETIME, description='Filter programs ending at or after this time. ISO 8601 format.'),
@ -780,8 +780,14 @@ def _build_q_object(field_name, term, use_regex=False, whole_words=False):
# Use Django's __iregex (case-insensitive regex)
return Q(**{f'{field_name}__iregex': term})
elif whole_words:
# Use word boundary regex (case-insensitive)
pattern = r'\b' + re.escape(term) + r'\b'
# Word boundary syntax differs by database engine:
# PostgreSQL uses \y (or \m/\M); Python re (SQLite) uses \b.
from django.db import connection
if connection.vendor == 'postgresql':
boundary = r'\y'
else:
boundary = r'\b'
pattern = boundary + re.escape(term) + boundary
return Q(**{f'{field_name}__iregex': pattern})
else:
# Standard case-insensitive contains

View file

@ -191,14 +191,22 @@ class ProgramSearchAPIViewTests(TestCase):
self.assertEqual(data["results"], [])
def test_title_whole_word_matching(self):
"""title_whole_words=true does not match partial words."""
# 'new' as substring matches both 'Newcastle vs Villa' and 'BBC News at Ten'
"""title_whole_words=true matches complete words but not partial words."""
# 'new' as substring matches 'Newcastle vs Villa' and 'BBC News at Ten'
partial = self.client.get(SEARCH_URL, {"title": "new"}).json()
whole = self.client.get(SEARCH_URL, {"title": "new", "title_whole_words": "true"}).json()
# icontains matches 'new' inside 'Newcastle' and 'News'
# Whole-word \bnew\b matches neither 'Newcastle' nor 'News' (partial matches)
whole_no_match = self.client.get(SEARCH_URL, {"title": "new", "title_whole_words": "true"}).json()
self.assertEqual(partial["count"], 2)
# Whole-word \bnew\b matches neither 'Newcastle' nor 'News'
self.assertEqual(whole["count"], 0)
self.assertEqual(whole_no_match["count"], 0)
# 'football' is a complete word in 'Premier League Football' — must still match
whole_match = self.client.get(SEARCH_URL, {"title": "football", "title_whole_words": "true"}).json()
self.assertEqual(whole_match["count"], 1)
self.assertEqual(whole_match["results"][0]["title"], "Premier League Football")
# 'league' is also a complete word — and 'Premier AND league' with whole_words works
both_words = self.client.get(SEARCH_URL, {"title": "Premier AND league", "title_whole_words": "true"}).json()
self.assertEqual(both_words["count"], 1)
def test_title_regex(self):
"""title_regex=true applies the query as a regex pattern."""