Refactor searches module

remotes/origin/enhancement/email-actions
David Barragán Merino 2014-06-16 15:40:51 +02:00
parent ab9043a7d7
commit d6546dc518
9 changed files with 243 additions and 122 deletions

View File

@ -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:

View File

@ -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

View File

@ -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")

78
taiga/searches/api.py Normal file
View File

@ -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

View File

@ -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]

View File

@ -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):

View File

@ -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