search_indexes.py 4.91 KB
Newer Older
1 2
""" Search index used to load data into elasticsearch"""

3 4 5
import logging
from elasticsearch.exceptions import ConnectionError

6
from django.conf import settings
7
from django.db.models.signals import post_delete, post_save
8
from django.dispatch import receiver
9
from django.utils import translation
10
from functools import wraps
11 12

from search.search_engine_base import SearchEngine
13
from request_cache import get_request_or_stub
14

15
from .errors import ElasticSearchConnectionError
16 17
from lms.djangoapps.teams.models import CourseTeam
from .serializers import CourseTeamSerializer
18 19


20 21 22 23 24 25 26 27 28 29 30 31 32
def if_search_enabled(f):
    """
    Only call `f` if search is enabled for the CourseTeamIndexer.
    """
    @wraps(f)
    def wrapper(*args, **kwargs):
        """Wraps the decorated function."""
        cls = args[0]
        if cls.search_is_enabled():
            return f(*args, **kwargs)
    return wrapper


33 34 35 36 37 38
class CourseTeamIndexer(object):
    """
    This is the index object for searching and storing CourseTeam model instances.
    """
    INDEX_NAME = "course_team_index"
    DOCUMENT_TYPE_NAME = "course_team"
39
    ENABLE_SEARCH_KEY = "ENABLE_TEAMS"
40 41 42 43 44 45 46 47 48 49 50 51

    def __init__(self, course_team):
        self.course_team = course_team

    def data(self):
        """
        Uses the CourseTeamSerializer to create a serialized course_team object.
        Adds in additional text and pk fields.
        Removes membership relation.

        Returns serialized object with additional search fields.
        """
52 53 54 55 56 57 58 59 60
        # Django Rest Framework v3.1 requires that we pass the request to the serializer
        # so it can construct hyperlinks.  To avoid changing the interface of this object,
        # we retrieve the request from the request cache.
        context = {
            "request": get_request_or_stub()
        }

        serialized_course_team = CourseTeamSerializer(self.course_team, context=context).data

61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76
        # Save the primary key so we can load the full objects easily after we search
        serialized_course_team['pk'] = self.course_team.pk
        # Don't save the membership relations in elasticsearch
        serialized_course_team.pop('membership', None)

        # add generally searchable content
        serialized_course_team['content'] = {
            'text': self.content_text()
        }

        return serialized_course_team

    def content_text(self):
        """
        Generate the text field used for general search.
        """
77 78
        # Always use the English version of any localizable strings (see TNL-3239)
        with translation.override('en'):
79 80 81
            return u"{name}\n{description}\n{country}\n{language}".format(
                name=self.course_team.name,
                description=self.course_team.description,
82 83 84
                country=self.course_team.country.name.format(),
                language=self._language_name()
            )
85 86 87 88 89 90 91 92 93 94 95 96

    def _language_name(self):
        """
        Convert the language from code to long name.
        """
        languages = dict(settings.ALL_LANGUAGES)
        try:
            return languages[self.course_team.language]
        except KeyError:
            return self.course_team.language

    @classmethod
97
    @if_search_enabled
98 99 100 101
    def index(cls, course_team):
        """
        Update index with course_team object (if feature is enabled).
        """
102 103 104 105 106 107 108 109 110 111 112
        search_engine = cls.engine()
        serialized_course_team = CourseTeamIndexer(course_team).data()
        search_engine.index(cls.DOCUMENT_TYPE_NAME, [serialized_course_team])

    @classmethod
    @if_search_enabled
    def remove(cls, course_team):
        """
        Remove course_team from the index (if feature is enabled).
        """
        cls.engine().remove(cls.DOCUMENT_TYPE_NAME, [course_team.team_id])
113 114

    @classmethod
115
    @if_search_enabled
116 117 118 119
    def engine(cls):
        """
        Return course team search engine (if feature is enabled).
        """
120
        try:
121
            return SearchEngine.get_search_engine(index=cls.INDEX_NAME)
122 123 124
        except ConnectionError as err:
            logging.error('Error connecting to elasticsearch: %s', err)
            raise ElasticSearchConnectionError
125 126 127 128 129 130 131 132 133

    @classmethod
    def search_is_enabled(cls):
        """
        Return boolean of whether course team indexing is enabled.
        """
        return settings.FEATURES.get(cls.ENABLE_SEARCH_KEY, False)


134
@receiver(post_save, sender=CourseTeam, dispatch_uid='teams.signals.course_team_post_save_callback')
135 136 137 138
def course_team_post_save_callback(**kwargs):
    """
    Reindex object after save.
    """
139 140 141 142
    try:
        CourseTeamIndexer.index(kwargs['instance'])
    except ElasticSearchConnectionError:
        pass
143 144 145 146 147 148 149


@receiver(post_delete, sender=CourseTeam, dispatch_uid='teams.signals.course_team_post_delete_callback')
def course_team_post_delete_callback(**kwargs):  # pylint: disable=invalid-name
    """
    Reindex object after delete.
    """
150 151 152 153
    try:
        CourseTeamIndexer.remove(kwargs['instance'])
    except ElasticSearchConnectionError:
        pass