Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion search/elastic.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,6 @@ def _process_filters(filter_dictionary):
},
}


def _process_exclude_dictionary(exclude_dictionary):
"""
Build a list of term fields which will be excluded from result set.
Expand Down
41 changes: 38 additions & 3 deletions search/filter_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,52 @@

from .utils import _load_class, DateRange

from typing import Any, Dict, List, Optional, Union

class SearchFilterGenerator:

"""
Class to provide a set of filters for the search.
Users of this search app will override this class and update setting for SEARCH_FILTER_GENERATOR
"""
@staticmethod
def _normalise_to_list(value: Any) -> List[Any]:
"""
Return *value* as a list without mutating it if it already is one.
"""
if isinstance(value, (list, tuple, set)):
return list(value)
return [value]

def filter_dictionary(self, *, field_filters=None, **_kwargs):
"""
Build search filters by adding a default start-date range and converting
provided field values into term or terms filters, depending on whether
each field has one or multiple values.

Parameters
----------
field_filters : dict, optional
A mapping of field names to raw filter values. Each value may be
singular or an iterable; values are normalized to lists.

def filter_dictionary(self, **kwargs):
""" base implementation which filters via start_date """
return {"start_date": DateRange(None, datetime.utcnow())}
Returns
-------
dict
A dictionary containing date and field-based filters suitable for use
in search queries.
"""
filters = {
"start_date": DateRange(None, datetime.utcnow())
}
if field_filters:
for field, raw in field_filters.items():
values = self._normalise_to_list(raw)
if len(values) == 1:
filters[field] = {"term": {f"{field}.keyword": values[0]}}
else:
filters[field] = {"terms": {f"{field}.keyword": values}}
return filters

def field_dictionary(self, **kwargs):
""" base implementation which add course if provided """
Expand Down
93 changes: 57 additions & 36 deletions search/views.py
Original file line number Diff line number Diff line change
@@ -1,48 +1,68 @@
""" handle requests for courseware search http requests """
# This contains just the url entry points to use if desired, which currently has only one

import json
import logging

from django.conf import settings
from django.http import JsonResponse
from django.http import JsonResponse, QueryDict
from django.utils.translation import gettext as _
from django.views.decorators.http import require_POST

from eventtracking import tracker as track
from .api import perform_search, course_discovery_search, course_discovery_filter_fields
from .initializer import SearchInitializer

# log appears to be standard name used for logger
log = logging.getLogger(__name__)


def _process_pagination_values(request):
""" process pagination requests from request parameter """
size = 20
page = 0
from_ = 0
if "page_size" in request.POST:
size = int(request.POST["page_size"])
max_page_size = getattr(settings, "SEARCH_MAX_PAGE_SIZE", 100)
# The parens below are superfluous, but make it much clearer to the reader what is going on
if not (0 < size <= max_page_size): # pylint: disable=superfluous-parens
raise ValueError(_('Invalid page size of {page_size}').format(page_size=size))

if "page_index" in request.POST:
page = int(request.POST["page_index"])
from_ = page * size
def parse_post_data(request):
"""Support both JSON and form-encoded input."""
if request.content_type == 'application/json':
try:
body = json.loads(request.body.decode('utf-8'))
except json.JSONDecodeError:
log.warning("⚠️ Malformed JSON received")
return QueryDict('', mutable=True)

qdict = QueryDict('', mutable=True)
for key, value in body.items():
if isinstance(value, list):
for item in value:
qdict.appendlist(key, item)
else:
qdict.update({key: value})
return qdict
return request.POST

def _process_pagination_values(data):
"""Extract pagination info from data."""
DEFAULT_PAGE_SIZE = 20
DEFAULT_PAGE_INDEX = 0
DEFAULT_MAX_PAGE_SIZE = 100
max_page_size = getattr(settings, "SEARCH_MAX_PAGE_SIZE", DEFAULT_MAX_PAGE_SIZE)

size = int(data.get("page_size", DEFAULT_PAGE_SIZE))
page = int(data.get("page_index", DEFAULT_PAGE_INDEX))

if not (0 < size <= max_page_size):
raise ValueError(_('Invalid page size of {page_size}').format(page_size=size))

from_ = page * size
return size, from_, page


def _process_field_values(request, is_multivalue=False):
""" Create separate dictionary of supported filter values provided """
get_value = request.POST.getlist if is_multivalue else request.POST.get
return {
field_key: get_value(field_key)
for field_key in request.POST
if field_key in course_discovery_filter_fields()
}

# ----to support multiple values per key as QueryDict and regular dict like "org=astu&org=openedx" ----
def _process_field_values(data):
filters = {}
for key in course_discovery_filter_fields():
if isinstance(data, QueryDict):
values = data.getlist(key)
else:
values = data.get(key, [])
if not isinstance(values, list):
values = [values]
if values:
filters[key] = values
return filters

@require_POST
def do_search(request, course_id=None):
Expand Down Expand Up @@ -179,21 +199,23 @@ def _course_discovery(request, is_multivalue=False):
"error": _("Nothing to search")
}
status_code = 500

search_term = request.POST.get("search_string", None)
enable_course_sorting_by_start_date = request.POST.get("enable_course_sorting_by_start_date", False)

# search_term = request.POST.get("search_string", None)
post_data = parse_post_data(request)
search_term = post_data.get("search_string", "").strip()
try:
size, from_, page = _process_pagination_values(request)
field_dictionary = _process_field_values(request, is_multivalue=is_multivalue)

size, from_, page = _process_pagination_values(post_data)
field_dictionary = _process_field_values(post_data)
# ✅ Allow searches even if search_string is empty, as long as filters are applied
if not search_term and not field_dictionary:
search_term = None
# Analytics - log search request
track.emit(
'edx.course_discovery.search.initiated',
{
"search_term": search_term,
"page_size": size,
"page_number": page,
"filters": field_dictionary, #track filters information
}
)

Expand All @@ -202,7 +224,6 @@ def _course_discovery(request, is_multivalue=False):
size=size,
from_=from_,
field_dictionary=field_dictionary,
enable_course_sorting_by_start_date=enable_course_sorting_by_start_date,
is_multivalue=is_multivalue,
)

Expand Down
Loading