diff --git a/taiga/base/filters.py b/taiga/base/filters.py index e70b8390..d8010e31 100644 --- a/taiga/base/filters.py +++ b/taiga/base/filters.py @@ -18,6 +18,8 @@ import logging +from dateutil.parser import parse as parse_date + from django.apps import apps from django.contrib.contenttypes.models import ContentType from django.db.models import Q @@ -447,6 +449,68 @@ class WatchersFilter(FilterBackend): return super().filter_queryset(request, queryset, view) +class BaseCompareFilter(FilterBackend): + operators = ["", "lt", "gt", "lte", "gte"] + + def __init__(self, filter_name_base=None, operators=None): + if filter_name_base: + self.filter_name_base = filter_name_base + + def _get_filter_names(self): + return [ + self._get_filter_name(operator) + for operator in self.operators + ] + + def _get_filter_name(self, operator): + if operator and len(operator) > 0: + return "{base}__{operator}".format( + base=self.filter_name_base, operator=operator + ) + else: + return self.filter_name_base + + def _get_constraints(self, params): + constraints = {} + for filter_name in self._get_filter_names(): + raw_value = params.get(filter_name, None) + if raw_value is not None: + constraints[filter_name] = self._get_value(raw_value) + return constraints + + def _get_value(self, raw_value): + return raw_value + + def filter_queryset(self, request, queryset, view): + constraints = self._get_constraints(request.QUERY_PARAMS) + + if len(constraints) > 0: + queryset = queryset.filter(**constraints) + + return super().filter_queryset(request, queryset, view) + + +class BaseDateFilter(BaseCompareFilter): + def _get_value(self, raw_value): + return parse_date(raw_value) + + +class CreatedDateFilter(BaseDateFilter): + filter_name_base = "created_date" + + +class ModifiedDateFilter(BaseDateFilter): + filter_name_base = "modified_date" + + +class FinishedDateFilter(BaseDateFilter): + filter_name_base = "finished_date" + + +class FinishDateFilter(BaseDateFilter): + filter_name_base = "finish_date" + + ##################################################################### # Text search filters ##################################################################### diff --git a/taiga/projects/issues/api.py b/taiga/projects/issues/api.py index 093b3ad1..08533e24 100644 --- a/taiga/projects/issues/api.py +++ b/taiga/projects/issues/api.py @@ -58,7 +58,10 @@ class IssueViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, W filters.TagsFilter, filters.WatchersFilter, filters.QFilter, - filters.OrderByFilterMixin) + filters.OrderByFilterMixin, + filters.CreatedDateFilter, + filters.ModifiedDateFilter, + filters.FinishedDateFilter) retrieve_exclude_filters = (filters.OwnersFilter, filters.AssignedToFilter, filters.StatusesFilter, diff --git a/tests/integration/test_issues.py b/tests/integration/test_issues.py index 5a0cc00c..a42715cd 100644 --- a/tests/integration/test_issues.py +++ b/tests/integration/test_issues.py @@ -19,6 +19,10 @@ import uuid import csv +import pytz + +from datetime import datetime, timedelta +from urllib.parse import quote from unittest import mock @@ -221,6 +225,103 @@ def test_api_filter_by_text_6(client): assert number_of_issues == 1 +def test_api_filter_by_created_date(client): + user = f.UserFactory(is_superuser=True) + one_day_ago = datetime.now(pytz.utc) - timedelta(days=1) + + old_issue = f.create_issue(owner=user, created_date=one_day_ago) + issue = f.create_issue(owner=user) + + url = reverse("issues-list") + "?created_date=%s" % ( + quote(issue.created_date.isoformat()) + ) + + client.login(issue.owner) + response = client.get(url) + number_of_issues = len(response.data) + + assert response.status_code == 200 + assert number_of_issues == 1 + assert response.data[0]["ref"] == issue.ref + + +def test_api_filter_by_created_date__gt(client): + user = f.UserFactory(is_superuser=True) + one_day_ago = datetime.now(pytz.utc) - timedelta(days=1) + + old_issue = f.create_issue(owner=user, created_date=one_day_ago) + issue = f.create_issue(owner=user) + + url = reverse("issues-list") + "?created_date__gt=%s" % ( + quote(one_day_ago.isoformat()) + ) + + client.login(issue.owner) + response = client.get(url) + number_of_issues = len(response.data) + + assert response.status_code == 200 + assert number_of_issues == 1 + assert response.data[0]["ref"] == issue.ref + + +def test_api_filter_by_created_date__gte(client): + user = f.UserFactory(is_superuser=True) + one_day_ago = datetime.now(pytz.utc) - timedelta(days=1) + + old_issue = f.create_issue(owner=user, created_date=one_day_ago) + issue = f.create_issue(owner=user) + + url = reverse("issues-list") + "?created_date__gte=%s" % ( + quote(one_day_ago.isoformat()) + ) + + client.login(issue.owner) + response = client.get(url) + number_of_issues = len(response.data) + + assert response.status_code == 200 + assert number_of_issues == 2 + + +def test_api_filter_by_created_date__lt(client): + user = f.UserFactory(is_superuser=True) + one_day_ago = datetime.now(pytz.utc) - timedelta(days=1) + + old_issue = f.create_issue(owner=user, created_date=one_day_ago) + issue = f.create_issue(owner=user) + + url = reverse("issues-list") + "?created_date__lt=%s" % ( + quote(issue.created_date.isoformat()) + ) + + client.login(issue.owner) + response = client.get(url) + number_of_issues = len(response.data) + + assert response.status_code == 200 + assert response.data[0]["ref"] == old_issue.ref + + +def test_api_filter_by_created_date__lte(client): + user = f.UserFactory(is_superuser=True) + one_day_ago = datetime.now(pytz.utc) - timedelta(days=1) + + old_issue = f.create_issue(owner=user, created_date=one_day_ago) + issue = f.create_issue(owner=user) + + url = reverse("issues-list") + "?created_date__lte=%s" % ( + quote(issue.created_date.isoformat()) + ) + + client.login(issue.owner) + response = client.get(url) + number_of_issues = len(response.data) + + assert response.status_code == 200 + assert number_of_issues == 2 + + def test_api_filters_data(client): project = f.ProjectFactory.create() user1 = f.UserFactory.create(is_superuser=True)