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.

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