diff --git a/AUTHORS.rst b/AUTHORS.rst index c7aff636..0f61b4da 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -29,6 +29,7 @@ answer newbie questions, and generally made taiga that much better: - Joe Letts - Julien Palard - luyikei +- Michael Jurke - Motius GmbH - Riccardo Coccioli - Ricky Posner diff --git a/CHANGELOG.md b/CHANGELOG.md index b2890f4e..50dae19b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,9 @@ - Improve messages generated on webhooks input. - Add mentions support in commit messages. - Cleanup hooks code. +- Add created-, modified-, finished- and finish_date queryset filters + - Support exact match, gt, gte, lt, lte + - added issues, tasks and userstories accordingly ### Misc - [API] Improve performance of some calls over list. 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/taiga/projects/tasks/api.py b/taiga/projects/tasks/api.py index 232d496e..3cf43913 100644 --- a/taiga/projects/tasks/api.py +++ b/taiga/projects/tasks/api.py @@ -56,7 +56,10 @@ class TaskViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, filters.StatusesFilter, filters.TagsFilter, filters.WatchersFilter, - filters.QFilter) + filters.QFilter, + filters.CreatedDateFilter, + filters.ModifiedDateFilter, + filters.FinishedDateFilter) retrieve_exclude_filters = (filters.OwnersFilter, filters.AssignedToFilter, filters.StatusesFilter, diff --git a/taiga/projects/userstories/api.py b/taiga/projects/userstories/api.py index 61911d61..3b6e108a 100644 --- a/taiga/projects/userstories/api.py +++ b/taiga/projects/userstories/api.py @@ -64,7 +64,10 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi filters.TagsFilter, filters.WatchersFilter, filters.QFilter, - filters.OrderByFilterMixin) + filters.OrderByFilterMixin, + filters.CreatedDateFilter, + filters.ModifiedDateFilter, + filters.FinishDateFilter) 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..b2055bc4 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,149 @@ 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_filter_by_modified_date__gte(client): + user = f.UserFactory(is_superuser=True) + _day_ago = datetime.now(pytz.utc) - timedelta(days=1) + + older_issue = f.create_issue(owner=user) + issue = f.create_issue(owner=user) + # we have to refresh as it slightly differs + issue.refresh_from_db() + + assert older_issue.modified_date < issue.modified_date + + url = reverse("issues-list") + "?modified_date__gte=%s" % ( + quote(issue.modified_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_finished_date(client): + user = f.UserFactory(is_superuser=True) + project = f.ProjectFactory.create() + status0 = f.IssueStatusFactory.create(project=project, is_closed=True) + + issue = f.create_issue(owner=user) + finished_issue = f.create_issue(owner=user, status=status0) + + assert finished_issue.finished_date + + url = reverse("issues-list") + "?finished_date__gte=%s" % ( + quote(finished_issue.finished_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"] == finished_issue.ref + + def test_api_filters_data(client): project = f.ProjectFactory.create() user1 = f.UserFactory.create(is_superuser=True) diff --git a/tests/integration/test_tasks.py b/tests/integration/test_tasks.py index c13add00..c9fd4d01 100644 --- a/tests/integration/test_tasks.py +++ b/tests/integration/test_tasks.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 @@ -451,6 +455,110 @@ def test_get_tasks_including_attachments(client): assert len(response.data[0].get("attachments")) == 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_task = f.create_task(owner=user, created_date=one_day_ago) + task = f.create_task(owner=user, subject="test") + + url = reverse("tasks-list") + "?created_date=%s" % ( + quote(task.created_date.isoformat()) + ) + + client.login(task.owner) + response = client.get(url) + number_of_tasks = len(response.data) + + assert response.status_code == 200 + assert number_of_tasks == 1 + assert response.data[0]["subject"] == task.subject + + +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_task = f.create_task(owner=user, created_date=one_day_ago) + task = f.create_task(owner=user, subject="test") + + url = reverse("tasks-list") + "?created_date__lt=%s" % ( + quote(task.created_date.isoformat()) + ) + + client.login(task.owner) + response = client.get(url) + number_of_tasks = len(response.data) + + assert response.status_code == 200 + assert response.data[0]["subject"] == old_task.subject + + +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_task = f.create_task(owner=user, created_date=one_day_ago) + task = f.create_task(owner=user) + + url = reverse("tasks-list") + "?created_date__lte=%s" % ( + quote(task.created_date.isoformat()) + ) + + client.login(task.owner) + response = client.get(url) + number_of_tasks = len(response.data) + + assert response.status_code == 200 + assert number_of_tasks == 2 + + +def test_api_filter_by_modified_date__gte(client): + user = f.UserFactory(is_superuser=True) + _day_ago = datetime.now(pytz.utc) - timedelta(days=1) + + older_task = f.create_task(owner=user) + task = f.create_task(owner=user, subject="test") + # we have to refresh as it slightly differs + task.refresh_from_db() + + assert older_task.modified_date < task.modified_date + + url = reverse("tasks-list") + "?modified_date__gte=%s" % ( + quote(task.modified_date.isoformat()) + ) + + client.login(task.owner) + response = client.get(url) + number_of_tasks = len(response.data) + + assert response.status_code == 200 + assert number_of_tasks == 1 + assert response.data[0]["subject"] == task.subject + + +def test_api_filter_by_finished_date(client): + user = f.UserFactory(is_superuser=True) + project = f.ProjectFactory.create() + status0 = f.TaskStatusFactory.create(project=project, is_closed=True) + + task = f.create_task(owner=user) + finished_task = f.create_task(owner=user, status=status0, subject="test") + + assert finished_task.finished_date + + url = reverse("tasks-list") + "?finished_date__gte=%s" % ( + quote(finished_task.finished_date.isoformat()) + ) + client.login(task.owner) + response = client.get(url) + number_of_tasks = len(response.data) + + assert response.status_code == 200 + assert number_of_tasks == 1 + assert response.data[0]["subject"] == finished_task.subject + + def test_api_filters_data(client): project = f.ProjectFactory.create() user1 = f.UserFactory.create(is_superuser=True) diff --git a/tests/integration/test_userstories.py b/tests/integration/test_userstories.py index b777b186..c9cd4bb1 100644 --- a/tests/integration/test_userstories.py +++ b/tests/integration/test_userstories.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 from django.core.urlresolvers import reverse @@ -501,6 +505,112 @@ def test_get_total_points(client): assert us_mixed.get_total_points() == 1.0 +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_userstory = f.create_userstory(owner=user, created_date=one_day_ago) + userstory = f.create_userstory(owner=user, subject="test") + + url = reverse("userstories-list") + "?created_date=%s" % ( + quote(userstory.created_date.isoformat()) + ) + + client.login(userstory.owner) + response = client.get(url) + number_of_userstories = len(response.data) + + assert response.status_code == 200 + assert number_of_userstories == 1 + assert response.data[0]["subject"] == userstory.subject + + +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_userstory = f.create_userstory( + owner=user, created_date=one_day_ago, subject="old test" + ) + userstory = f.create_userstory(owner=user) + + url = reverse("userstories-list") + "?created_date__lt=%s" % ( + quote(userstory.created_date.isoformat()) + ) + + client.login(userstory.owner) + response = client.get(url) + number_of_userstories = len(response.data) + + assert response.status_code == 200 + assert response.data[0]["subject"] == old_userstory.subject + + +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_userstory = f.create_userstory(owner=user, created_date=one_day_ago) + userstory = f.create_userstory(owner=user) + + url = reverse("userstories-list") + "?created_date__lte=%s" % ( + quote(userstory.created_date.isoformat()) + ) + + client.login(userstory.owner) + response = client.get(url) + number_of_userstories = len(response.data) + + assert response.status_code == 200 + assert number_of_userstories == 2 + + +def test_api_filter_by_modified_date__gte(client): + user = f.UserFactory(is_superuser=True) + + older_userstory = f.create_userstory(owner=user) + userstory = f.create_userstory(owner=user, subject="test") + # we have to refresh as it slightly differs + userstory.refresh_from_db() + + assert older_userstory.modified_date < userstory.modified_date + + url = reverse("userstories-list") + "?modified_date__gte=%s" % ( + quote(userstory.modified_date.isoformat()) + ) + + client.login(userstory.owner) + response = client.get(url) + number_of_userstories = len(response.data) + + assert response.status_code == 200 + assert number_of_userstories == 1 + assert response.data[0]["subject"] == userstory.subject + + +def test_api_filter_by_finish_date(client): + user = f.UserFactory(is_superuser=True) + one_day_later = datetime.now(pytz.utc) + timedelta(days=1) + + userstory = f.create_userstory(owner=user) + userstory_to_finish = f.create_userstory( + owner=user, finish_date=one_day_later, subject="test" + ) + + assert userstory_to_finish.finish_date + + url = reverse("userstories-list") + "?finish_date__gte=%s" % ( + quote(userstory_to_finish.finish_date.isoformat()) + ) + client.login(userstory.owner) + response = client.get(url) + number_of_userstories = len(response.data) + + assert response.status_code == 200 + assert number_of_userstories == 1 + assert response.data[0]["subject"] == userstory_to_finish.subject + + def test_api_filters_data(client): project = f.ProjectFactory.create() user1 = f.UserFactory.create(is_superuser=True)