Django Library PaTT
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

search.py 4.8KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171
  1. import datetime
  2. from django.conf import settings
  3. import fstools
  4. import logging
  5. from .models import Task, TASKSTATE_OPEN, TASKSTATE_FINISHED, TASKSTATE_CLOSED, TASKSTATE_CANCELED
  6. import os
  7. import re
  8. from whoosh.fields import Schema, ID, TEXT, NUMERIC, DATETIME, BOOLEAN
  9. from whoosh.qparser.dateparse import DateParserPlugin
  10. from whoosh import index, qparser
  11. try:
  12. from config import APP_NAME as ROOT_LOGGER_NAME
  13. except ImportError:
  14. ROOT_LOGGER_NAME = 'root'
  15. logger = logging.getLogger(ROOT_LOGGER_NAME).getChild(__name__)
  16. SEARCH_MY_OPEN_TASKS = 1
  17. SEARCH_LOST_SOULS = 2
  18. def common_searches(request):
  19. cs = {}
  20. if request.user.is_authenticated:
  21. cs[SEARCH_MY_OPEN_TASKS] = ('My Open Tasks', mk_search_pattern(user_ids=[request.user.id]))
  22. cs[SEARCH_LOST_SOULS] = ('Lost Souls (no user)', 'assigned_user_missing:1')
  23. return cs
  24. # INDEX_STATES
  25. #
  26. INDEX_STATES = {
  27. TASKSTATE_OPEN: 'Open',
  28. TASKSTATE_FINISHED: 'Finished',
  29. TASKSTATE_CLOSED: 'Closed',
  30. TASKSTATE_CANCELED: 'Canselled'
  31. }
  32. SCHEMA = Schema(
  33. id=ID(unique=True, stored=True),
  34. # Task
  35. task_id=NUMERIC,
  36. assigned_user=TEXT,
  37. assigned_user_missing=BOOLEAN,
  38. name=TEXT,
  39. description=TEXT,
  40. state=TEXT,
  41. targetdate=DATETIME,
  42. # Related Project
  43. project_id=NUMERIC,
  44. project_name=TEXT,
  45. project_description=TEXT,
  46. # Related Comments
  47. comment=TEXT,
  48. )
  49. def mk_whooshpath_if_needed():
  50. if not os.path.exists(settings.WHOOSH_PATH):
  51. fstools.mkdir(settings.WHOOSH_PATH)
  52. def create_index():
  53. mk_whooshpath_if_needed()
  54. logger.debug('Search Index created.')
  55. return index.create_in(settings.WHOOSH_PATH, schema=SCHEMA)
  56. def load_index():
  57. mk_whooshpath_if_needed()
  58. try:
  59. ix = index.open_dir(settings.WHOOSH_PATH)
  60. except index.EmptyIndexError:
  61. ix = create_index()
  62. else:
  63. logger.debug('Search Index opened.')
  64. return ix
  65. def add_item(ix, item):
  66. # Define Standard data
  67. #
  68. data = dict(
  69. id='%d' % item.id,
  70. # Task
  71. task_id=item.id,
  72. name=item.name,
  73. description=item.description,
  74. state=INDEX_STATES.get(item.state),
  75. # Related Project
  76. project_id=item.project.id,
  77. project_name=item.project.name,
  78. project_description=item.project.description,
  79. # Related Comments
  80. comment=' '.join([c.comment for c in item.comment_set.all()]),
  81. )
  82. # Add Optional data
  83. #
  84. if item.assigned_user is not None:
  85. data['assigned_user'] = item.assigned_user.username
  86. data['assigned_user_missing'] = False
  87. else:
  88. data['assigned_user_missing'] = True
  89. if item.targetdate is not None:
  90. data['targetdate'] = datetime.datetime.combine(item.targetdate, datetime.datetime.min.time())
  91. # Write data to the index
  92. #
  93. with ix.writer() as w:
  94. logger.info('Adding document with id=%d to the search index.', data.get('task_id'))
  95. w.add_document(**data)
  96. for key in data:
  97. logger.debug(' - Adding %s=%s', key, repr(data[key]))
  98. def delete_item(ix, item):
  99. with ix.writer() as w:
  100. logger.info('Removing document with id=%d from the search index.', item.id)
  101. w.delete_by_term("task_id", item.id)
  102. def update_item(ix, item):
  103. delete_item(ix, item)
  104. add_item(ix, item)
  105. def rebuild_index(ix):
  106. for t in Task.objects.all():
  107. add_item(ix, t)
  108. return len(Task.objects.all())
  109. def search(ix, search_txt):
  110. qp = qparser.MultifieldParser(['name', 'description'], ix.schema)
  111. qp.add_plugin(DateParserPlugin(free=True))
  112. try:
  113. q = qp.parse(search_txt)
  114. except AttributeError:
  115. return None
  116. except Exception:
  117. return None
  118. with ix.searcher() as s:
  119. results = s.search(q, limit=None)
  120. rpl = []
  121. for hit in results:
  122. rpl.append(hit['id'])
  123. return Task.objects.filter(id__in=rpl)
  124. def mk_search_pattern(**kwargs):
  125. prj_ids = kwargs.get('prj_ids', [])
  126. user_ids = kwargs.get('user_ids', [])
  127. states = kwargs.get('states', [INDEX_STATES.get(TASKSTATE_OPEN), INDEX_STATES.get(TASKSTATE_FINISHED)])
  128. rule_parts = []
  129. if prj_ids is not None and len(prj_ids) > 0:
  130. rule_parts.append(' OR '.join(['project_id:%s' % pid for pid in prj_ids]))
  131. if user_ids is not None and len(user_ids) > 0:
  132. from django.contrib.auth.models import User
  133. rule_parts.append(' OR '.join(['assigned_user:%s' % User.objects.get(id=uid).username for uid in user_ids]))
  134. if states is not None and len(states) > 0:
  135. rule_parts.append(' OR '.join(['state:%s' % state for state in states]))
  136. return ' AND '.join('(%s)' % rule for rule in rule_parts)
  137. def get_project_ids_from_search_pattern(search_txt):
  138. try:
  139. return re.findall('project_id:(\d+)', search_txt)
  140. except AttributeError:
  141. return None
  142. except TypeError:
  143. return None