@@ -101,16 +101,27 @@ def get_events(request):
101101
102102 if search_term :
103103 import re
104+ from django .db .models import Func , Value , F
104105
105106 # Parse semicolon-separated filters (for OR query)
106107 search_terms = [
107108 term .strip () for term in search_term .split (";" ) if term .strip ()
108109 ]
109110
110- # Build OR query: match any of the search terms in any field
111+ # Define character mapping for translate
112+ # Common accented chars
113+ accents_from = "àáâãäåèéêëìíîïòóôõöùúûüýÿñç"
114+ accents_to = "aaaaaaeeeeiiiiooooouuuuyync"
115+
116+ # Special chars to remove
117+ special_from = "-_.,!?:;()[]{}|/\\ "
118+ map_from = accents_from + special_from
119+ map_to = accents_to
120+
121+ # Build OR query
111122 or_queries = Q ()
112123 for term in search_terms :
113- # Search with original term (exact/partial matches)
124+ # 1. Standard search (exact/partial matches)
114125 term_query = (
115126 Q (title__icontains = term )
116127 | Q (location__icontains = term )
@@ -125,20 +136,39 @@ def get_events(request):
125136 | Q (fb_handle__icontains = term )
126137 )
127138
128- # Search with normalized term
139+ # 2. Normalized search using translate
129140 normalized_term = re .sub (r'[^a-z0-9]' , '' , term .lower ())
130141 if normalized_term and normalized_term != term .lower ():
131- term_query |= (
132- Q (title__icontains = normalized_term )
133- | Q (location__icontains = normalized_term )
134- | Q (description__icontains = normalized_term )
135- | Q (food__icontains = normalized_term )
136- | Q (club_type__icontains = normalized_term )
137- | Q (ig_handle__icontains = normalized_term )
138- | Q (discord_handle__icontains = normalized_term )
139- | Q (x_handle__icontains = normalized_term )
140- | Q (tiktok_handle__icontains = normalized_term )
141- | Q (fb_handle__icontains = normalized_term )
142+ # Func for translate:
143+ # translate(lower(field), map_from, map_to)
144+ class Translate (Func ):
145+ function = 'TRANSLATE'
146+ template = "%(function)s(LOWER(%(expressions)s), '%(from)s', '%(to)s')"
147+
148+ # Use .extra() to filter on normalized fields (Django doesn't support function calls in filter keys)
149+ translate_sql = f"TRANSLATE(LOWER({{}}), '{ map_from } ', '{ map_to } ') LIKE %s"
150+ like_param = f"%{ normalized_term } %"
151+ fields = [
152+ "title" , "location" , "description" , "food" ,
153+ "club_type" , "ig_handle" , "discord_handle" ,
154+ "x_handle" , "tiktok_handle" , "fb_handle"
155+ ]
156+ where_clauses = []
157+ params = []
158+ for field in fields :
159+ # Handle COALESCE for nullable fields
160+ col_ref = field
161+ if field not in ["title" ]: # title is nullable but usually present, others might be null
162+ col_ref = f"COALESCE({ field } , '')"
163+ where_clauses .append (translate_sql .format (col_ref ))
164+ params .append (like_param )
165+
166+ full_where = " OR " .join (where_clauses )
167+ term_query |= Q (
168+ id__in = filtered_queryset .extra (
169+ where = [full_where ],
170+ params = params
171+ ).values_list ('id' , flat = True )
142172 )
143173
144174 or_queries |= term_query
0 commit comments