search.py 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
  1. from urlparse import urlparse
  2. from django.core.urlresolvers import resolve
  3. from django.db.models import Q
  4. from django.http import Http404
  5. from django.utils.translation import ugettext_lazy as _
  6. from haystack.inputs import AutoQuery
  7. from haystack.query import SearchQuerySet, RelatedSearchQuerySet
  8. from misago.acl.exceptions import ACLError403, ACLError404
  9. from misago.models import Forum, Thread, Post, User
  10. class SearchException(Exception):
  11. def __init__(self, message=None, suggestion=None):
  12. self.message = message
  13. self.suggestion = suggestion
  14. def __unicode__(self):
  15. return self.message
  16. class SearchQuery(object):
  17. def __init__(self, raw_query=None):
  18. """
  19. Build search query object
  20. """
  21. if raw_query:
  22. self.parse_query(raw_query)
  23. def parse_query(self, raw_query):
  24. """
  25. Parse raw search query into dict of lists of words that should be found and cant be found in string
  26. """
  27. self.criteria = {'+': [], '-': []}
  28. for word in unicode(raw_query).split():
  29. # Trim word and skip it if its empty
  30. word = unicode(word).strip().lower()
  31. if len(word) == 0:
  32. pass
  33. # Find word mode
  34. mode = '+'
  35. if word[0] == '-':
  36. mode = '-'
  37. word = unicode(word[1:]).strip()
  38. # Strip extra crap
  39. word = ''.join(e for e in word if e.isalnum())
  40. # Slice word?
  41. if len(word) <= 3:
  42. raise SearchException(_("One or more search phrases are shorter than four characters."))
  43. if mode == '+':
  44. if len(word) == 5:
  45. word = word[0:-1]
  46. if len(word) == 6:
  47. word = word[0:-2]
  48. if len(word) > 6:
  49. word = word[0:-3]
  50. self.criteria[mode].append(word)
  51. # Complain that there are no positive matches
  52. if not self.criteria['+'] and not self.criteria['-']:
  53. raise SearchException(_("Search query is invalid."))
  54. def search(self, value):
  55. """
  56. See if value meets search criteria, return True for success and False otherwhise
  57. """
  58. try:
  59. value = unicode(value).strip().lower()
  60. # Search for only
  61. if self.criteria['+'] and not self.criteria['-']:
  62. return self.search_for(value)
  63. # Search against only
  64. if self.criteria['-'] and not self.criteria['+']:
  65. return self.search_against(value)
  66. # Search if contains for values but not against values
  67. return self.search_for(value) and not self.search_against(value)
  68. except AttributeError:
  69. raise SearchException(_("You have to define search query before you will be able to search."))
  70. def search_for(self, value):
  71. """
  72. See if value is required
  73. """
  74. for word in self.criteria['+']:
  75. if value.find(word) != -1:
  76. return True
  77. return False
  78. def search_against(self, value):
  79. """
  80. See if value is forbidden
  81. """
  82. for word in self.criteria['-']:
  83. if value.find(word) != -1:
  84. return True
  85. return False
  86. class MisagoSearchQuerySet(object):
  87. def __init__(self, user, acl):
  88. self._content = None
  89. self._thread_start = None
  90. self._thread_name = None
  91. self._user_name = None
  92. self._after = None
  93. self._before = None
  94. self._children = None
  95. self._threads = None
  96. self._forums = None
  97. self.user = user
  98. self.acl = acl
  99. def search_in(self, target):
  100. try:
  101. self.allow_forum_search(target)
  102. except AttributeError:
  103. self.allow_thread_search(target)
  104. def allow_forum_search(self, forum):
  105. if forum.special == 'private_threads':
  106. if not self.acl.private_threads.can_participate():
  107. raise ACLError403()
  108. if self.acl.private_threads.is_mod():
  109. self._threads = [t.pk for t in forum.thread_set.filter(Q(participants__id=self.user.pk) | Q(replies_reported__gt=0)).iterator()]
  110. else:
  111. self._threads = [t.pk for t in forum.thread_set.filter(participants__id=self.user.pk).iterator()]
  112. elif forum.special == 'reports':
  113. if not self.acl.reports.can_handle():
  114. raise ACLError403()
  115. self._forums = [forum.pk]
  116. else:
  117. self._forums = Forum.objects.readable_forums(self.acl)
  118. def allow_thread_search(self, thread):
  119. self.allow_forum_search(thread.forum)
  120. if thread.forum.special == 'private_threads':
  121. if thread.pk in self._threads:
  122. self._threads = [thread.pk]
  123. else:
  124. self._threads = [-1]
  125. self._threads = [thread.pk]
  126. def search_content(self, query):
  127. self._content = query
  128. def restrict_threads(self, threads=None):
  129. self._threads = threads
  130. def search_thread_name_link(self, query):
  131. try:
  132. link = resolve(urlparse(query).path)
  133. thread = Thread.objects.get(pk=link.kwargs['thread'])
  134. self.allow_thread_search(thread)
  135. except (Http404, KeyError, Thread.DoesNotExist):
  136. self._thread_name = query
  137. def search_thread_titles(self, value=True):
  138. self._thread_start = value
  139. def search_thread_name(self, query):
  140. self._thread_name = query
  141. def search_user_name(self, query):
  142. self._user_name = query
  143. def search_after(self, datetime):
  144. self._after = datetime
  145. def search_before(self, datetime):
  146. self._before = datetime
  147. def in_forums(self, forums):
  148. self._forums = forums
  149. @property
  150. def query(self):
  151. try:
  152. return self._searchquery
  153. except AttributeError:
  154. pass
  155. sqs = SearchQuerySet()
  156. if self._content:
  157. sqs = sqs.auto_query(self._content)
  158. if self._thread_name:
  159. sqs = sqs.filter(thread_name=AutoQuery(self._thread_name))
  160. if self._thread_start:
  161. sqs = sqs.filter(start_post=1)
  162. if self._user_name:
  163. sqs = sqs.filter(username=self._user_name)
  164. if self._before:
  165. sqs = sqs.filter(date__lte=self._before)
  166. if self._after:
  167. sqs = sqs.filter(date__gte=self._after)
  168. if self._threads:
  169. sqs = sqs.filter(thread__in=self._threads)
  170. if self._forums:
  171. sqs = sqs.filter(forum__in=self._forums)
  172. self._searchquery = sqs
  173. return self._searchquery