Refactor searches module
parent
ab9043a7d7
commit
d6546dc518
|
@ -167,7 +167,6 @@ INSTALLED_APPS = [
|
|||
"django.contrib.staticfiles",
|
||||
|
||||
"taiga.base",
|
||||
"taiga.base.searches",
|
||||
"taiga.events",
|
||||
"taiga.front",
|
||||
"taiga.users",
|
||||
|
@ -183,6 +182,7 @@ INSTALLED_APPS = [
|
|||
"taiga.projects.history",
|
||||
"taiga.projects.notifications",
|
||||
"taiga.projects.votes",
|
||||
"taiga.searches",
|
||||
"taiga.timeline",
|
||||
"taiga.mdrender",
|
||||
|
||||
|
@ -294,9 +294,11 @@ REST_FRAMEWORK = {
|
|||
DEFAULT_PROJECT_TEMPLATE = "scrum"
|
||||
PUBLIC_REGISTER_ENABLED = False
|
||||
|
||||
SEARCHES_MAX_RESULTS = 150
|
||||
|
||||
# NOTE: DON'T INSERT MORE SETTINGS AFTER THIS LINE
|
||||
|
||||
TEST_RUNNER="django.test.runner.DiscoverRunner"
|
||||
#TEST_RUNNER="django.test.runner.DiscoverRunner"
|
||||
|
||||
# Test conditions
|
||||
if "test" in sys.argv:
|
||||
|
|
|
@ -1,118 +0,0 @@
|
|||
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be>
|
||||
# Copyright (C) 2014 Jesús Espino <jespinog@gmail.com>
|
||||
# Copyright (C) 2014 David Barragán <bameda@dbarragan.com>
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from django.db.models.loading import get_model
|
||||
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import viewsets
|
||||
|
||||
from taiga.base import exceptions as excp
|
||||
from taiga.projects.userstories.serializers import UserStorySerializer
|
||||
from taiga.projects.tasks.serializers import TaskSerializer
|
||||
from taiga.projects.issues.serializers import IssueSerializer
|
||||
from taiga.projects.wiki.serializers import WikiPageSerializer
|
||||
|
||||
|
||||
class SearchViewSet(viewsets.ViewSet):
|
||||
def list(self, request, **kwargs):
|
||||
project_model = get_model("projects", "Project")
|
||||
text = request.QUERY_PARAMS.get('text', "")
|
||||
get_all = request.QUERY_PARAMS.get('get_all', False)
|
||||
project_id = request.QUERY_PARAMS.get('project', None)
|
||||
|
||||
try:
|
||||
project = self._get_project(project_id)
|
||||
except (project_model.DoesNotExist, TypeError):
|
||||
raise excp.PermissionDenied({"detail": "Wrong project id"})
|
||||
|
||||
result = {
|
||||
"userstories": self._search_user_stories(project, text, get_all),
|
||||
"tasks": self._search_tasks(project, text, get_all),
|
||||
"issues": self._search_issues(project, text, get_all),
|
||||
"wikipages": self._search_wiki_pages(project, text, get_all)
|
||||
}
|
||||
|
||||
result["count"] = sum(map(lambda x: len(x), result.values()))
|
||||
return Response(result)
|
||||
|
||||
def _get_project(self, project_id):
|
||||
project_model = get_model("projects", "Project")
|
||||
own_projects = (project_model.objects
|
||||
.filter(members=self.request.user))
|
||||
|
||||
return own_projects.get(pk=project_id)
|
||||
|
||||
def _search_user_stories(self, project, text, get_all):
|
||||
where_clause = ("to_tsvector(userstories_userstory.subject || "
|
||||
"userstories_userstory.description) @@ plainto_tsquery(%s)")
|
||||
|
||||
model_cls = get_model("userstories", "UserStory")
|
||||
if get_all != "false" and text == '':
|
||||
queryset = model_cls.objects.filter(project_id=project.pk)
|
||||
else:
|
||||
queryset = (model_cls.objects
|
||||
.extra(where=[where_clause], params=[text])
|
||||
.filter(project_id=project.pk)[:50])
|
||||
|
||||
serializer = UserStorySerializer(queryset, many=True)
|
||||
return serializer.data
|
||||
|
||||
def _search_tasks(self, project, text, get_all):
|
||||
where_clause = ("to_tsvector(tasks_task.subject || tasks_task.description) "
|
||||
"@@ plainto_tsquery(%s)")
|
||||
|
||||
model_cls = get_model("tasks", "Task")
|
||||
if get_all != "false" and text == '':
|
||||
queryset = model_cls.objects.filter(project_id=project.pk)
|
||||
else:
|
||||
queryset = (model_cls.objects
|
||||
.extra(where=[where_clause], params=[text])
|
||||
.filter(project_id=project.pk)[:50])
|
||||
|
||||
serializer = TaskSerializer(queryset, many=True)
|
||||
return serializer.data
|
||||
|
||||
def _search_issues(self, project, text, get_all):
|
||||
where_clause = ("to_tsvector(issues_issue.subject || issues_issue.description) "
|
||||
"@@ plainto_tsquery(%s)")
|
||||
|
||||
model_cls = get_model("issues", "Issue")
|
||||
if get_all != "false" and text == '':
|
||||
queryset = model_cls.objects.filter(project_id=project.pk)
|
||||
else:
|
||||
queryset = (model_cls.objects
|
||||
.extra(where=[where_clause], params=[text])
|
||||
.filter(project_id=project.pk)[:50])
|
||||
|
||||
serializer = IssueSerializer(queryset, many=True)
|
||||
return serializer.data
|
||||
|
||||
def _search_wiki_pages(self, project, text, get_all):
|
||||
where_clause = ("to_tsvector(wiki_wikipage.slug || wiki_wikipage.content) "
|
||||
"@@ plainto_tsquery(%s)")
|
||||
|
||||
model_cls = get_model("wiki", "WikiPage")
|
||||
if get_all != "false" and text == '':
|
||||
queryset = model_cls.objects.filter(project_id=project.pk)
|
||||
else:
|
||||
queryset = (model_cls.objects
|
||||
.extra(where=[where_clause], params=[text])
|
||||
.filter(project_id=project.pk)[:50])
|
||||
|
||||
serializer = WikiPageSerializer(queryset, many=True)
|
||||
return serializer.data
|
||||
|
||||
|
|
@ -32,11 +32,15 @@ from taiga.userstorage.api import StorageEntriesViewSet
|
|||
router.register(r"user-storage", StorageEntriesViewSet, base_name="user-storage")
|
||||
|
||||
|
||||
# Resolver & Search
|
||||
from taiga.base.searches.api import SearchViewSet
|
||||
# Resolver
|
||||
from taiga.projects.references.api import ResolverViewSet
|
||||
|
||||
router.register(r"resolver", ResolverViewSet, base_name="resolver")
|
||||
|
||||
|
||||
# Search
|
||||
from taiga.searches.api import SearchViewSet
|
||||
|
||||
router.register(r"search", SearchViewSet, base_name="search")
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,78 @@
|
|||
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be>
|
||||
# Copyright (C) 2014 Jesús Espino <jespinog@gmail.com>
|
||||
# Copyright (C) 2014 David Barragán <bameda@dbarragan.com>
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from django.db.models.loading import get_model
|
||||
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import viewsets
|
||||
|
||||
from taiga.base import exceptions as excp
|
||||
from taiga.projects.userstories.serializers import UserStorySerializer
|
||||
from taiga.projects.tasks.serializers import TaskSerializer
|
||||
from taiga.projects.issues.serializers import IssueSerializer
|
||||
from taiga.projects.wiki.serializers import WikiPageSerializer
|
||||
|
||||
from . import services
|
||||
|
||||
|
||||
class SearchViewSet(viewsets.ViewSet):
|
||||
def list(self, request, **kwargs):
|
||||
project_model = get_model("projects", "Project")
|
||||
|
||||
text = request.QUERY_PARAMS.get('text', "")
|
||||
project_id = request.QUERY_PARAMS.get('project', None)
|
||||
|
||||
try:
|
||||
project = self._get_project(project_id)
|
||||
except (project_model.DoesNotExist, TypeError):
|
||||
raise excp.PermissionDenied({"detail": "Wrong project id"})
|
||||
|
||||
result = {
|
||||
"userstories": self._search_user_stories(project, text),
|
||||
"tasks": self._search_tasks(project, text),
|
||||
"issues": self._search_issues(project, text),
|
||||
"wikipages": self._search_wiki_pages(project, text)
|
||||
}
|
||||
|
||||
result["count"] = sum(map(lambda x: len(x), result.values()))
|
||||
return Response(result)
|
||||
|
||||
def _get_project(self, project_id):
|
||||
project_model = get_model("projects", "Project")
|
||||
own_projects = (project_model.objects
|
||||
.filter(members=self.request.user))
|
||||
|
||||
return own_projects.get(pk=project_id)
|
||||
|
||||
def _search_user_stories(self, project, text):
|
||||
queryset = services.search_user_stories(project, text)
|
||||
serializer = UserStorySerializer(queryset, many=True)
|
||||
return serializer.data
|
||||
|
||||
def _search_tasks(self, project, text):
|
||||
queryset = services.search_tasks(project, text)
|
||||
serializer = TaskSerializer(queryset, many=True)
|
||||
return serializer.data
|
||||
|
||||
def _search_issues(self, project, text):
|
||||
queryset = services.search_issues(project, text)
|
||||
serializer = IssueSerializer(queryset, many=True)
|
||||
return serializer.data
|
||||
|
||||
def _search_wiki_pages(self, project, text):
|
||||
queryset = services.search_wiki_pages(project, text)
|
||||
serializer = WikiPageSerializer(queryset, many=True)
|
||||
return serializer.data
|
|
@ -0,0 +1,69 @@
|
|||
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be>
|
||||
# Copyright (C) 2014 Jesús Espino <jespinog@gmail.com>
|
||||
# Copyright (C) 2014 David Barragán <bameda@dbarragan.com>
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from django.db.models.loading import get_model
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
MAX_RESULTS = getattr(settings, "SEARCHES_MAX_RESULTS", 150)
|
||||
|
||||
|
||||
def search_user_stories(project, text):
|
||||
model_cls = get_model("userstories", "UserStory")
|
||||
where_clause = ("to_tsvector(coalesce(userstories_userstory.subject) || ' ' || "
|
||||
"coalesce(userstories_userstory.description)) @@ plainto_tsquery(%s)")
|
||||
|
||||
if text:
|
||||
return (model_cls.objects.extra(where=[where_clause], params=[text])
|
||||
.filter(project_id=project.pk)[:MAX_RESULTS])
|
||||
|
||||
return model_cls.objects.filter(project_id=project.pk)[:MAX_RESULTS]
|
||||
|
||||
|
||||
def search_tasks(project, text):
|
||||
model_cls = get_model("tasks", "Task")
|
||||
where_clause = ("to_tsvector(coalesce(tasks_task.subject, '') || ' ' || coalesce(tasks_task.description, '')) "
|
||||
"@@ plainto_tsquery(%s)")
|
||||
|
||||
if text:
|
||||
return (model_cls.objects.extra(where=[where_clause], params=[text])
|
||||
.filter(project_id=project.pk)[:MAX_RESULTS])
|
||||
|
||||
return model_cls.objects.filter(project_id=project.pk)[:MAX_RESULTS]
|
||||
|
||||
|
||||
def search_issues(project, text):
|
||||
model_cls = get_model("issues", "Issue")
|
||||
where_clause = ("to_tsvector(coalesce(issues_issue.subject) || ' ' || coalesce(issues_issue.description)) "
|
||||
"@@ plainto_tsquery(%s)")
|
||||
|
||||
if text:
|
||||
return (model_cls.objects.extra(where=[where_clause], params=[text])
|
||||
.filter(project_id=project.pk)[:MAX_RESULTS])
|
||||
|
||||
return model_cls.objects.filter(project_id=project.pk)[:MAX_RESULTS]
|
||||
|
||||
|
||||
def search_wiki_pages(project, text):
|
||||
model_cls = get_model("wiki", "WikiPage")
|
||||
where_clause = ("to_tsvector(coalesce(wiki_wikipage.slug) || ' ' || coalesce(wiki_wikipage.content)) "
|
||||
"@@ plainto_tsquery(%s)")
|
||||
|
||||
if text:
|
||||
return (model_cls.objects.extra(where=[where_clause], params=[text])
|
||||
.filter(project_id=project.pk)[:MAX_RESULTS])
|
||||
|
||||
return model_cls.objects.filter(project_id=project.pk)[:MAX_RESULTS]
|
|
@ -110,6 +110,7 @@ class UserStoryFactory(Factory):
|
|||
project = factory.SubFactory("tests.factories.ProjectFactory")
|
||||
owner = factory.SubFactory("tests.factories.UserFactory")
|
||||
subject = factory.Sequence(lambda n: "User Story {}".format(n))
|
||||
description = factory.Sequence(lambda n: "User Story {} description".format(n))
|
||||
|
||||
|
||||
class MilestoneFactory(Factory):
|
||||
|
@ -124,6 +125,7 @@ class IssueFactory(Factory):
|
|||
FACTORY_FOR = get_model("issues", "Issue")
|
||||
|
||||
subject = factory.Sequence(lambda n: "Issue {}".format(n))
|
||||
description = factory.Sequence(lambda n: "Issue {} description".format(n))
|
||||
owner = factory.SubFactory("tests.factories.UserFactory")
|
||||
project = factory.SubFactory("tests.factories.ProjectFactory")
|
||||
status = factory.SubFactory("tests.factories.IssueStatusFactory")
|
||||
|
@ -137,6 +139,7 @@ class TaskFactory(Factory):
|
|||
FACTORY_FOR = get_model("tasks", "Task")
|
||||
|
||||
subject = factory.Sequence(lambda n: "Task {}".format(n))
|
||||
description = factory.Sequence(lambda n: "Task {} description".format(n))
|
||||
owner = factory.SubFactory("tests.factories.UserFactory")
|
||||
project = factory.SubFactory("tests.factories.ProjectFactory")
|
||||
status = factory.SubFactory("tests.factories.TaskStatusFactory")
|
||||
|
@ -148,6 +151,8 @@ class WikiPageFactory(Factory):
|
|||
|
||||
project = factory.SubFactory("tests.factories.ProjectFactory")
|
||||
owner = factory.SubFactory("tests.factories.UserFactory")
|
||||
slug = factory.Sequence(lambda n: "wiki-page-{}".format(n))
|
||||
content = factory.Sequence(lambda n: "Wiki Page {} content".format(n))
|
||||
|
||||
|
||||
class IssueStatusFactory(Factory):
|
||||
|
|
|
@ -0,0 +1,81 @@
|
|||
import pytest
|
||||
|
||||
from django.core.urlresolvers import reverse
|
||||
|
||||
from .. import factories as f
|
||||
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
@pytest.fixture
|
||||
def searches_initial_data():
|
||||
m = type("InitialData", (object,), {})()
|
||||
|
||||
m.project1 = f.ProjectFactory.create()
|
||||
m.project2 = f.ProjectFactory.create()
|
||||
|
||||
m.member1 = f.MembershipFactory.create(project=m.project1)
|
||||
m.member2 = f.MembershipFactory.create(project=m.project1)
|
||||
|
||||
m.us1 = f.UserStoryFactory.create(project=m.project1)
|
||||
m.us2 = f.UserStoryFactory.create(project=m.project1, description="Back to the future")
|
||||
m.us3 = f.UserStoryFactory.create(project=m.project2)
|
||||
|
||||
m.tsk1 = f.TaskFactory.create(project=m.project2)
|
||||
m.tsk2 = f.TaskFactory.create(project=m.project1)
|
||||
m.tsk3 = f.TaskFactory.create(project=m.project1, subject="Back to the future")
|
||||
|
||||
m.iss1 = f.IssueFactory.create(project=m.project1, subject="Backend and Frontend")
|
||||
m.iss2 = f.IssueFactory.create(project=m.project2)
|
||||
m.iss3 = f.IssueFactory.create(project=m.project1)
|
||||
|
||||
m.wiki1 = f.WikiPageFactory.create(project=m.project1)
|
||||
m.wiki2 = f.WikiPageFactory.create(project=m.project1, content="Frontend, future")
|
||||
m.wiki3 = f.WikiPageFactory.create(project=m.project2)
|
||||
|
||||
return m
|
||||
|
||||
|
||||
def test_search_all_objects_in_my_project(client, searches_initial_data):
|
||||
data = searches_initial_data
|
||||
|
||||
client.login(data.member1.user)
|
||||
|
||||
response = client.get(reverse("search-list"), {"project": data.project1.id})
|
||||
assert response.status_code == 200
|
||||
assert response.data["count"] == 8
|
||||
assert len(response.data["userstories"]) == 2
|
||||
assert len(response.data["tasks"]) == 2
|
||||
assert len(response.data["issues"]) == 2
|
||||
assert len(response.data["wikipages"]) == 2
|
||||
|
||||
|
||||
def test_search_all_objects_in_project_is_not_mine(client, searches_initial_data):
|
||||
data = searches_initial_data
|
||||
|
||||
client.login(data.member1.user)
|
||||
|
||||
response = client.get(reverse("search-list"), {"project": data.project2.id})
|
||||
assert response.status_code == 403
|
||||
|
||||
|
||||
def test_search_text_query_in_my_project(client, searches_initial_data):
|
||||
data = searches_initial_data
|
||||
|
||||
client.login(data.member1.user)
|
||||
|
||||
response = client.get(reverse("search-list"), {"project": data.project1.id, "text": "future"})
|
||||
assert response.status_code == 200
|
||||
assert response.data["count"] == 3
|
||||
assert len(response.data["userstories"]) == 1
|
||||
assert len(response.data["tasks"]) == 1
|
||||
assert len(response.data["issues"]) == 0
|
||||
assert len(response.data["wikipages"]) == 1
|
||||
|
||||
response = client.get(reverse("search-list"), {"project": data.project1.id, "text": "back"})
|
||||
assert response.status_code == 200
|
||||
assert response.data["count"] == 2
|
||||
assert len(response.data["userstories"]) == 1
|
||||
assert len(response.data["tasks"]) == 1
|
||||
assert len(response.data["issues"]) == 0
|
||||
assert len(response.data["wikipages"]) == 0
|
Loading…
Reference in New Issue