diff --git a/taiga/base/filters.py b/taiga/base/filters.py
index cd802eee..a0b994a1 100644
--- a/taiga/base/filters.py
+++ b/taiga/base/filters.py
@@ -13,7 +13,8 @@
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-
+import operator
+from functools import reduce
from django.db.models import Q
from django.db.models.sql.where import ExtraWhere, OR
@@ -214,3 +215,28 @@ class TagsFilter(FilterBackend):
queryset = tags.filter(queryset, contains=query_tags)
return super().filter_queryset(request, queryset, view)
+
+
+class SearchFieldFilter(filters.SearchFilter):
+ """Search filter that looks up the search param in the parameter named after the search field,
+ that is: ?=... instead of looking for the search param: ?search=...
+ This way you can search in a field-specific way.
+ """
+ def get_search_terms(self, request, field):
+ params = request.QUERY_PARAMS.get(field, '')
+ return params.replace(',', ' ').split()
+
+ def filter_queryset(self, request, queryset, view):
+ search_fields = getattr(view, "search_fields", None)
+ if not search_fields:
+ return queryset
+
+ lookups = dict((self.construct_search(field), self.get_search_terms(request, field))
+ for field in search_fields)
+
+ for lookup, values in lookups.items():
+ or_queries = [Q(**{lookup: value}) for value in values]
+ if or_queries:
+ queryset = queryset.filter(reduce(operator.or_, or_queries))
+
+ return queryset
diff --git a/taiga/projects/userstories/api.py b/taiga/projects/userstories/api.py
index 667178df..78ef81ac 100644
--- a/taiga/projects/userstories/api.py
+++ b/taiga/projects/userstories/api.py
@@ -48,9 +48,11 @@ class UserStoryViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMi
list_serializer_class = serializers.UserStorySerializer
permission_classes = (permissions.UserStoryPermission,)
- filter_backends = (filters.CanViewUsFilterBackend, filters.TagsFilter)
+ filter_backends = (filters.CanViewUsFilterBackend, filters.TagsFilter,
+ filters.SearchFieldFilter)
retrieve_exclude_filters = (filters.TagsFilter,)
filter_fields = ['project', 'milestone', 'milestone__isnull', 'status', 'is_archived']
+ search_fields = ('subject',)
# Specific filter used for filtering neighbor user stories
_neighbor_tags_filter = filters.TagsFilter('neighbor_tags')
diff --git a/tests/integration/test_userstories.py b/tests/integration/test_userstories.py
index 9a508261..a127b972 100644
--- a/tests/integration/test_userstories.py
+++ b/tests/integration/test_userstories.py
@@ -51,3 +51,16 @@ def test_api_delete_userstory(client):
response = client.delete(url)
assert response.status_code == 204
+
+
+def test_api_filter_by_subject(client):
+ f.create_userstory()
+ us = f.create_userstory(subject="some random subject")
+ url = reverse("userstories-list") + "?subject=some subject"
+
+ client.login(us.owner)
+ response = client.get(url)
+ number_of_stories = len(response.data)
+
+ assert response.status_code == 200
+ assert number_of_stories == 1, number_of_stories